Compare commits

...
Author SHA1 Message Date
levaliciousandClaude Opus 5.5 685d084af7 cni: lock each pod's port to the pod's own addresses
images / images (push) Successful in 7m6s
A pod could put any source MAC or address on the wire: weave has no layer-2
anti-spoofing, so a pod with NET_RAW could pose as another pod or as a node's
bridge, on its own node (local bridging never reaches the router) and, through
weave, on others. That undercut every rule that trusts a source MAC or address,
including the egress holder's check of which node a tunnel packet came from.

The CNI plugin now locks the host side of each pod's veth with tc ingress
filters: IPv4 from the pod's MAC and address, ARP speaking for exactly those
(or probing from 0.0.0.0) pass; everything else is dropped before the bridge
sees it. The u32 keys follow iproute2's layout for "ether src" (-8, -4) and
"ip src" (12). ADD fails if the port cannot be locked, so an unlocked pod never
starts. The filters go away with the veth.

Pods attached before this plugin get locked by weave-init: "weaveutil
lock-ports" walks libcni's result cache (each attachment's netns, interface and
address) and locks their ports, leaving locked ones alone. weave-init mounts
the cache and /var/run/netns read-only for it.

TestPortLockInKernel (root) builds a bridge and two pods in throwaway
namespaces and checks what reaches the other pod; reweave/test/portlock runs
the same against a live cluster, across nodes, after checking every port is
locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 22:51:16 -04:00
levaliciousandClaude Opus 5.5 9261a48983 lb: members' ping identifiers in their node's block too
images / images (push) Successful in 6m34s
The live run found ping from a member on one node failing right after a
member on another node pinged the same host: busybox ping uses its PID as
the ICMP echo identifier, containers' PIDs are small and alike, so both
nodes' echoes reached the holder as the same connection, and the holder
dropped the second as a collision (fail closed, as intended). NAT treats
the echo identifier as ICMP's port, and iptables accepts a range for it:
members' ICMP is now SNATed into the node's block like TCP, UDP and SCTP,
so every node's pings are distinct on the holder. Replies follow the
holder's tag as before.

The end-to-end test's ping and traceroute pods run as root (the curl
image's user cannot use raw sockets even with NET_RAW).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 19:57:22 -04:00
levaliciousandClaude Opus 5.5 b040f97149 ci: free the runner's disk after every build
images / images (push) Successful in 11m16s
The images go to the registry with --push, but each build also left its
tagged images in the runner's local store and an unbounded build cache:
after a day of builds the runner's 10Gi Docker volume filled up and run 21
died mid-build (no space left on device). A final step, run even when the
build fails, removes our pushed tags and caps the build cache at 4GB
(recent layers stay, so builds stay cached).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 19:39:08 -04:00
levaliciousandClaude Opus 5.5 602e2da73c lb: one egress tunnel per peer; replies and ICMP errors follow a tag
images / images (push) Failing after 9m54s
The holder could tell a member's node only by source port block, so
replies without ports never got back: ICMP errors (path-MTU "too big",
unreachable, time exceeded), ping, and port-less protocols. Now every
node pair has its own ipip tunnel (rweg-<hash of the peer>, between the
two weave bridges), so the holder knows the sending node by the device a
packet arrives on:

- it tags each connection arriving from a peer's tunnel with that peer
  (connmark: peer index in the top byte, plus 0x200000), once;
- replies and related ICMP errors restore the tag, and a policy rule per
  peer (ahead of the local table, like the port rules) routes them into
  that peer's tunnel, where the member node's conntrack translates them
  back to the member;
- an arrival on one peer's tunnel whose connection another peer already
  holds (a port-less protocol to the same remote from two nodes) is
  dropped, never delivered across;
- the stateless port-block rules stay, now for TCP, UDP and SCTP.

Members' SNAT keeps TCP, UDP and SCTP within the node's block and puts
everything else (ping, GRE, ...) on the address alone; the guard lets any
protocol into the holder's tunnel and nothing anywhere else. The holder
only accepts, per tunnel, the address with that node's ports.

Routes in the reply tables are now read with one dump per pass instead of
one per candidate table.

reweave/test/egress/run.sh also checks ping from every node, that ICMP
errors from routers beyond the holder reach a member (traceroute), and
that ping is dropped while nobody holds the address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 19:27:09 -04:00
levaliciousandClaude Opus 5.5 31bf46404a lb: delete default routes as listed (RouteDel refuses a nil Dst)
images / images (push) Successful in 17m38s
RouteList returns a default route without Dst, and netlink's RouteDel
refuses a route with no Dst, Src or Gw. The egress unreachable table's
"unreachable default" could not be removed once installed: every later
pass failed there, before updating rules, so members stayed dropped after
their EgressService got a holder again (fail closed, but stuck). S1's
stale reply tables had the same latent fault. Both now delete through
delRoute, which spells the default destination out; the unreachable route
is compared by type alone, so it is not replaced on every pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 16:01:59 -04:00
levaliciousandClaude Opus 5.5 ee1c2f905d lb, weave: three faults the first live EgressService run found
images / images (push) Successful in 17m57s
- The tunnel needs an address. Reverse-path filtering (loose, as on our
  nodes) rejects what arrives on a device without an IPv4 address unless
  the reverse route leaves by that very device: the holder dropped every
  member packet after decapsulation (ip route get: EXDEV), and a member's
  node would drop the replies. reweave-eg now carries the node's bridge
  address as a /32 (it never shows on the wire: the tunnel sets the outer
  source itself, and what crosses it is forwarded).
- The kernel leaves out the priority attribute of a rule at 0, so the
  default local rule lists as unset (-1) and moveLocalRule never found
  it: it stayed at 0 beside the new one at 100, and the holder delivered
  replies to its own address to itself (answering with RSTs). Unset now
  counts as 0.
- install_cni_plugin copied the plugin only when weave-plugin-$VERSION was
  missing, and every build of ours is "latest": nodes kept a plugin that
  ignores egressAdmission, so a new member's first connection left from
  its node. It is now replaced whenever its content differs (sha256),
  atomically.

Also: the end-to-end manifest's non-member label is a string ("none", not
YAML's boolean no).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 15:31:29 -04:00
levaliciousandClaude Opus 5.5 b413f2a5f8 lb: a failed pass blocks pod admission only where EgressServices exist
images / images (push) Successful in 19m11s
With no EgressService and no member rules installed, an admission has
nothing to protect or clean up, so a failing pass (a datapath bug, a
bridge not yet addressed) answers "not a member" instead of stopping
every pod on the node from starting. Wherever EgressServices exist,
admission still fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 14:40:32 -04:00
levaliciousandClaude Opus 5.5 305ac4f3d3 lb: EgressService - a LoadBalancer Service's pods leave from its address
images / images (push) Successful in 18m34s
EgressService (reweave.levk.dev/v1alpha1, named after a LoadBalancer
Service of class reweave.levk.dev/node-address) makes every pod the
Service selects (by pod IP, never readiness) leave the cluster from the
Service's address, or not at all. spec.emit is explicit: Holder (the only
mode so far) tunnels members' packets to the node holding the address,
which emits them.

The holder (S1's: the one node with the address on an interface) is the
status' sole writer: host, address, and a 512-port block per node in
61000-65535 (above the default ephemeral range), stable across churn.
A member's node SNATs it to address:its-block before it leaves, so the
holder forwards it unchanged and routes replies back by port alone: ip
rules (to address, ipproto tcp/udp, dport block) ahead of the local
table, which moves from 0 to 100, since the address is local on the
holder. Members' packets travel both ways in reweave-eg, an ipip tunnel
between weave bridges (NPC drops new routed packets into weave), with
accept_local and loose rp_filter on that device only.

Members' new connections are classed at the tail of nat PREROUTING,
after kube-proxy's DNAT has ended the nat walk for Services: the pod
network, multicast and the nodes' addresses are not outside. Marked
packets follow a per-member rule to the holder's table (or to an
unreachable one), are SNATed into the tunnel, and a FORWARD guard drops
anything else a member sends outside (other protocols, other ways out).
The tunnel is accepted only from the nodes' bridges, a holder accepts
from it only the blocks' ports, and TCP MSS is clamped into it.

Fail closed throughout: until the holder has published the status,
without a block, with no single holder, or for a pod two EgressServices
select, a member's outside traffic is dropped. The CNI plugin asks
weave-npc to admit every pod (unix socket /run/weave-npc/egress.sock,
conflist "egressAdmission": true, now rewritten when it differs) after
IPAM and before the pod has an interface, so a member's first packet
already leaves from the address; no answer, no pod. weave-npc applies
nothing about egress from caches that are not synced (unless the API is
absent): a restart never strips members' rules.

reweave/test/egress/run.sh checks the design's assertions against a
live cluster; both end-to-end scripts now re-evaluate each check on
every retry (it was expanded once, before the loop).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 14:39:44 -04:00
levaliciousandClaude Opus 5.5 8018ea4bf9 lb: keep the client's address past weave's masquerade
images / images (push) Successful in 19m13s
Weave's expose rule (nat WEAVE: ! -s <alloc> -d <alloc> -j MASQUERADE)
masquerades everything entering the pod network from outside unless it is
for a local pod (weaver-no-masq-local). Live on the SG cluster a pod on
another node than the holder saw the holder's bridge address instead of
the client's. A new chain, REWEAVE-LB-POST, hooked first in nat
POSTROUTING, lets a connection we DNATed to a held address leave the nat
table with its source untouched; one that comes from a pod (hairpin) is
masqueraded to the holder instead, so its reply returns through the
holder's conntrack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 13:30:08 -04:00
levaliciousandClaude Opus 5.5 ef50dc921f lb: DNAT rules carry their protocol; end-to-end test against a cluster
images / images (push) Successful in 14m35s
iptables refuses a DNAT to ip:port unless the same rule names the
protocol, so every per-port chain's DNAT now has -p. Live on the SG
cluster the first sync (no endpoints yet) applied; every later one failed
with "Need TCP, UDP, SCTP or DCCP with port specification" and left the
chains empty. The unit test now rejects any DNAT line without its
protocol; the rendered nat and mangle payloads were also checked with the
nodes' own iptables-restore --test (nf_tables).

reweave/test/lb/run.sh is the end-to-end test: from outside, both a pod on
the holder and one on another node see the client's own address; status
is [{ip, ipMode: Proxy}] and kube-proxy has nothing for the address; a pod
on a third node reaches it; an unheld address clears the status and stops
the forward; deleting the Services removes the holder's chains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 13:10:49 -04:00
levaliciousandClaude Opus 5.5 9bc3415111 lb: LoadBalancer Services from addresses nodes hold, client address kept
images / images (push) Successful in 14m46s
A Service of type LoadBalancer with loadBalancerClass
reweave.levk.dev/node-address names its address in spec.loadBalancerIP.
Each node's weave-npc publishes the IPv4 addresses on its host interfaces
and its weave bridge (Node annotations reweave.levk.dev/addresses and
reweave.levk.dev/weave). The one node that holds a Service's address is
its holder: it alone writes status.loadBalancer (ipMode Proxy, so
kube-proxy installs nothing for the address) and DNATs the address's ports
to ready endpoints (EndpointSlices), without SNAT, from nat PREROUTING
ahead of kube-proxy. Every other node marks the connections the holder
forwards to its pods (new, from the holder's bridge MAC, from outside the
pod network) and routes their replies back to the holder's bridge through
a per-holder policy rule and table, so the holder's conntrack undoes the
DNAT and the reply leaves from the address the client connected to.

Fail closed: an address no node holds, or that several nodes hold, is
served by no node, and its status is cleared (by its holder, or by the
first node by name if none holds it). Compute is the policy as a pure
function; Datapath replaces exactly the chains it owns with
iptables-restore --noflush and keeps its rules and tables exact.

weave-npc runs it unless --lb=false; the ClusterRole gains services
(watch), services/status (patch), endpointslices (watch), nodes (patch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-13 12:45:54 -04:00
levaliciousandClaude Opus 5.5 4cf88d674c ci: check out the tools submodule
images / images (push) Successful in 17m51s
reweave/build's `make all` also builds tools/runner from the tools
submodule (weaveworks/build-tools); run 7 compiled every weave binary and
then stopped there. Fetched from upstream for now, like the build's other
fetches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-12 17:42:52 -04:00
levaliciousandClaude Opus 5.5 5ae90e4796 build: take bullseye and buster packages from archive.debian.org
images / images (push) Failing after 9m40s
Debian has archived both: buster is gone from deb.debian.org and
bullseye's security pool there now 404s on packages its index still
lists (run 6: xz-utils, dpkg-dev, libdpkg-perl). archive.debian.org
serves both releases, including libpcap 1.8.1-6+deb10u1 for the static
link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-12 17:32:28 -04:00
levaliciousandClaude Opus 5.5 8d36dc2f9b ci: push images as the reweave-ci bot
images / images (push) Failing after 20s
Gitea's registry refuses the job token (no package scope at login), so log
in as reweave-ci with the org secret REGISTRY_TOKEN, and drop the
packages permission that had no effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-12 17:18:15 -04:00
levaliciousandClaude Opus 5.5 6260533b6d ci: build weave-kube and weave-npc, push to this Gitea's registry
images / images (push) Failing after 17s
A Gitea Actions workflow that builds the two images the cluster runs from
reweave/build/Dockerfile (linux/amd64 only) and pushes them to
git.levk.dev/reweave/{weave-kube,weave-npc}, tagged with the full commit
SHA so a tag never changes meaning. It stops at the push; rollout is the
CD side's job. Build-time fetches (base images, apt/apk, Go modules) stay
external for now and are to be peeled out once self-deploy works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHXzGrsxfkGk8LmwrBbHNZ
2026-10-12 17:12:04 -04:00
Raj Chaudhuri 60b710c6a8 Scan results on 2025-09-16 2025-09-16 19:17:09 +05:30
Raj Chaudhuri 8397222003 Add release v2.9.0 2024-12-22 20:54:31 +05:30
Raj Chaudhuri 999aeb7d0b Made changes to assume iptables-nft as the default backend for iptables, and provisioned for changing to legacy mode if required 2024-12-22 18:56:07 +05:30
Raj Chaudhuri 9057156d28 Upgraded two dependencies based on scan results 2024-12-22 18:01:42 +05:30
Raj Chaudhuri 2c67714a03 Scan results on 2024-12-22 2024-12-22 17:41:31 +05:30
Raj Chaudhuri 84afe28bee Add release v2.8.10 2024-10-17 17:25:04 +05:30
Raj Chaudhuri 6512da7cc1 Go version upgraded to 1.23.1 2024-10-17 14:41:56 +05:30
Raj Chaudhuri fab7297623 Scan results on 2024-10-17 2024-10-17 13:03:20 +05:30
Raj Chaudhuri ca2ba767d9 Alpine base image upgraded to 3.20.3 2024-10-17 12:16:12 +05:30
Raj Chaudhuri f32a2a6955 Scan results on 2024-10-16 2024-10-16 10:10:51 +05:30
Raj Chaudhuri 463c7af871 Add release v2.8.9 2024-08-09 13:22:24 +05:30
Raj Chaudhuri 981196a5e8 Updated a dependency, Go version and Alpine base image 2024-08-09 12:49:30 +05:30
Raj Chaudhuri ae5d4624b2 Scan results on 2024-08-09 2024-08-09 10:17:47 +05:30
Raj Chaudhuri 8f85c320af Scan results on 2024-07-30 2024-07-30 13:03:21 +05:30
Raj Chaudhuri 32ad2a1b02 Documentation updates 2024-07-19 08:54:16 +05:30
613 changed files with 71455 additions and 14132 deletions

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
# Build the images the cluster runs (weave-kube, weave-npc) and push them to
# this Gitea's container registry. CI stops here: rolling them out is the
# CD side's job, not this workflow's.
#
# Tags are the full commit SHA, so a tag never moves: rebuilding a commit
# pushes the same name again instead of changing what an existing name means.
# linux/amd64 only for now (every node is amd64).
name: images
on:
push:
paths-ignore:
- "website/**"
- "docs/**"
- "**.md"
workflow_dispatch:
# The job token can't log in to the registry (no package scope there), so
# the push uses the reweave-ci bot's token from the org secret REGISTRY_TOKEN.
permissions:
contents: read
jobs:
images:
runs-on: ubuntu-latest
env:
REGISTRY: git.levk.dev
IMAGE_PREFIX: git.levk.dev/reweave
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # tags, for the version string
submodules: true # tools/ (weaveworks/build-tools): make all builds tools/runner
- name: Log in to the registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u reweave-ci --password-stdin
- name: Build and push (linux/amd64)
run: |
set -eu
sha=$(git rev-parse HEAD)
version=$(git describe --tags --always | sed 's/^v//')
for pair in weavekubeimage:weave-kube weavenpcimage:weave-npc; do
target=${pair%%:*}
image="$IMAGE_PREFIX/${pair#*:}:$sha"
docker buildx build --push --progress=plain \
--platform=linux/amd64 \
--target="$target" \
--build-arg=WEAVE_VERSION="$version" \
--build-arg=revision="$sha" \
--build-arg=imageversion="$version" \
-f reweave/build/Dockerfile \
-t "$image" \
.
echo "pushed $image"
done
# The images live in the registry now; the runner keeps only a bounded build cache. Without this the
# runner's 10Gi Docker volume fills up after a handful of builds (no space left on device mid-build).
- name: Free the runner's disk
if: always()
run: |
set -u
docker image ls --format '{{.Repository}}:{{.Tag}}' | grep "^$IMAGE_PREFIX/" | xargs -r docker image rm -f
docker image prune -f
docker builder prune -f --keep-storage 4GB
docker system df
+14
View File
@@ -1,3 +1,17 @@
## Release 2.9.0
This release has upgraded dependencies based on CVE recommendations.
Also, from this release onwards, all weave images will use the `iptables-nft` backend for `iptables` by default. To use the legacy mode, set the environment variable `IPTABLES_BACKEND` to `legacy`. To do this for the Docker plugin, export the variable before using the `weave` script. For the Kubernetes addon, set the variable in the manifest.
## Release 2.8.10
This release bumps the Go version to 1.23.1 and the alpine base image to version 3.20.3 as per CVE recommendations.
## Release 2.8.9
This release bumps the Go version to 1.22.2, the alpine base image to version 3.20.2, and upgrades a dependency as per CVE recommendations.
## Release 2.8.8
The alpine base image used by `weave-kube` and `weave-npc` has been upgraded to version 3.20.0 in this release. This removes several CVEs.
+2 -2
View File
@@ -1,4 +1,4 @@
# ReWeave - an effort to keep Weave Net alive
# Weave Net
This repository contains a fork of Weave Net, the first product developed by Weaveworks. Since Weaveworks has shut down, this repo aims to continue maintaining Weave Net, and to publish releases regularly.
@@ -7,7 +7,7 @@ This repository contains a fork of Weave Net, the first product developed by Wea
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/rajch/weave?include_prereleases)](https://github.com/rajch/weave/releases)
[![Unique CVE count in all images](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Frajch%2Fweave%2Fmaster%2Freweave%2Fscans%2Fbadge.json&label=CVE%20count "The number of unique CVEs reported by scanning all images")](reweave/scans/report.md)
The history of the ReWeave effort can be found in [HISTORY.md](HISTORY.md).
The history of the fork can be found in [HISTORY.md](HISTORY.md).
## Using Weave Net on Kubernetes
+21 -13
View File
@@ -1,6 +1,6 @@
module github.com/rajch/weave
go 1.21
go 1.23.1
require (
github.com/andybalholm/go-bit v1.0.1
@@ -10,7 +10,7 @@ require (
github.com/containernetworking/cni v1.1.2
github.com/containernetworking/plugins v1.2.0
github.com/coreos/go-iptables v0.6.0
github.com/docker/docker v24.0.9+incompatible
github.com/docker/docker v25.0.6+incompatible
github.com/docker/libnetwork v0.8.0-dev.2.0.20180212220201-119badbd737b
github.com/docker/machine v0.14.0
github.com/fsouza/go-dockerclient v1.10.0
@@ -30,9 +30,9 @@ require (
github.com/weaveworks/go-checkpoint v0.0.0-20170503165305-ebbb8b0518ab
github.com/weaveworks/go-odp v0.0.0-20181017121109-6b0aa22550d9
github.com/weaveworks/mesh v0.0.0-20191105120815-58dbcc3e8e63
golang.org/x/crypto v0.21.0
golang.org/x/sys v0.18.0
golang.org/x/tools v0.10.0
golang.org/x/crypto v0.31.0
golang.org/x/sys v0.28.0
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
k8s.io/api v0.26.2
k8s.io/apimachinery v0.26.2
k8s.io/client-go v0.26.2
@@ -48,20 +48,23 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/containerd v1.7.11 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.2-beta.1+incompatible // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/emicklei/go-restful/v3 v3.10.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 // indirect
@@ -76,6 +79,7 @@ require (
github.com/mdlayher/raw v0.0.0-20181016155347-fa5ef3332ca9 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@@ -83,25 +87,29 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rootless-containers/rootlesskit v1.1.0 // indirect
github.com/safchain/ethtool v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.23.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/term v0.27.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
+60 -26
View File
@@ -58,6 +58,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -70,6 +72,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw=
github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ=
github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
github.com/containernetworking/plugins v1.2.0 h1:SWgg3dQG1yzUo4d9iD8cwSVh1VqI+bP7mkPDoSfP9VU=
@@ -83,10 +87,10 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.8.2-beta.1+incompatible h1:gILO60VLD2v28ozemv4aAwDb8ds5U2O/vD/sBXbd7Rw=
github.com/docker/distribution v2.8.2-beta.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg=
github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -104,6 +108,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
@@ -122,8 +128,11 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@@ -180,8 +189,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -204,6 +213,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo=
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -263,6 +274,8 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -300,8 +313,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss=
github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -337,8 +348,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rootless-containers/rootlesskit v1.1.0 h1:cRaRIYxY8oce4eE/zeAUZhgKu/4tU1p9YHN4+suwV7M=
github.com/rootless-containers/rootlesskit v1.1.0/go.mod h1:H+o9ndNe7tS91WqU0/+vpvc+VaCd7TCIWaJjnV0ujUo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.2.0 h1:dILxMBqDnQfX192cCAPjZr9v2IgVXeElHPy435Z/IdE=
github.com/safchain/ethtool v0.2.0/go.mod h1:WkKB1DnNtvsMlDmQ50sgwowDJV/hGbJSOvJoEXs1AJQ=
@@ -384,6 +393,22 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -391,8 +416,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -423,8 +448,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -460,8 +485,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -481,8 +506,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -523,6 +548,7 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -533,12 +559,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -546,8 +572,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -596,8 +622,9 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -656,6 +683,11 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -668,6 +700,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+87
View File
@@ -0,0 +1,87 @@
package lb
import (
"fmt"
"net"
"sort"
"strings"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
// Local is what this node publishes about itself, read from its interfaces.
type Local struct {
Addresses []net.IP
WeaveIP net.IP
WeaveMAC net.HardwareAddr
PodNet *net.IPNet // the weave allocation range: the bridge address's network
}
// overlay- and container-side interfaces: their addresses are never held ones
var overlayPrefixes = []string{"weave", "datapath", "vxlan-", "veth", "docker", "cni"}
func overlay(name string) bool {
for _, p := range overlayPrefixes {
if strings.HasPrefix(name, p) {
return true
}
}
return false
}
// ReadLocal lists the global IPv4 addresses on host interfaces, and the weave bridge's address and MAC.
func ReadLocal() (Local, error) {
var l Local
links, err := netlink.LinkList()
if err != nil {
return l, err
}
for _, link := range links {
name := link.Attrs().Name
addrs, err := netlink.AddrList(link, unix.AF_INET)
if err != nil {
return l, fmt.Errorf("%s: %v", name, err)
}
if name == bridge {
for _, a := range addrs {
if a.IPNet != nil && a.IP.To4() != nil {
l.WeaveIP = a.IP.To4()
l.WeaveMAC = link.Attrs().HardwareAddr
l.PodNet = &net.IPNet{IP: a.IP.Mask(a.Mask), Mask: a.Mask}
break
}
}
continue
}
if overlay(name) || link.Attrs().Flags&net.FlagLoopback != 0 {
continue
}
for _, a := range addrs {
if a.Scope == unix.RT_SCOPE_UNIVERSE && a.IP.To4() != nil && a.IP.IsGlobalUnicast() {
l.Addresses = append(l.Addresses, a.IP.To4())
}
}
}
// an address inside the pod network is never a host one (whatever interface it is on)
if l.PodNet != nil {
kept := l.Addresses[:0]
for _, ip := range l.Addresses {
if !l.PodNet.Contains(ip) {
kept = append(kept, ip)
}
}
l.Addresses = kept
}
sort.Slice(l.Addresses, func(i, j int) bool { return l.Addresses[i].String() < l.Addresses[j].String() })
return l, nil
}
// Annotations is what this node's Node object should carry for l.
func (l Local) Annotations() map[string]string {
a := map[string]string{AnnAddresses: FormatAddresses(l.Addresses)}
if l.WeaveIP != nil && l.WeaveMAC != nil {
a[AnnWeave] = l.WeaveIP.String() + "," + l.WeaveMAC.String()
}
return a
}
+88
View File
@@ -0,0 +1,88 @@
// Package admit is the protocol between the weave CNI plugin and weave-npc's egress agent: at CNI ADD the
// plugin tells the agent which pod got which IP, and the agent answers once the pod's egress rules are in
// place, so an EgressService member's first packet already leaves from the Service's address (or not at all).
//
// It depends on the standard library only: the plugin binary must not grow a Kubernetes client.
package admit
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
)
// SocketPath is where the agent listens (a hostPath directory shared with the host, where the plugin runs).
const SocketPath = "/run/weave-npc/egress.sock"
// Request is one pod's admission: the IP the plugin just allocated to it.
type Request struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
IP string `json:"ip"`
}
// Response says whether the pod is an EgressService member (its rules are installed either way when 200).
type Response struct {
Member bool `json:"member"`
}
// Timeout bounds one admission (the agent may have to fetch the pod from the API server).
const Timeout = 20 * time.Second
// PodFromArgs reads the pod's namespace and name from CNI_ARGS ("K8S_POD_NAMESPACE=..;K8S_POD_NAME=..").
func PodFromArgs(args string) (namespace, name string) {
for _, kv := range strings.Split(args, ";") {
k, v, ok := strings.Cut(kv, "=")
if !ok {
continue
}
switch k {
case "K8S_POD_NAMESPACE":
namespace = v
case "K8S_POD_NAME":
name = v
}
}
return namespace, name
}
// Admit asks the agent at socket to admit req, and returns only once the agent has installed the pod's rules.
func Admit(ctx context.Context, socket string, req Request) (Response, error) {
var resp Response
body, err := json.Marshal(req)
if err != nil {
return resp, err
}
client := &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", socket)
},
}}
ctx, cancel := context.WithTimeout(ctx, Timeout)
defer cancel()
hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://weave-npc/admit", bytes.NewReader(body))
if err != nil {
return resp, err
}
hreq.Header.Set("Content-Type", "application/json")
hresp, err := client.Do(hreq)
if err != nil {
return resp, fmt.Errorf("egress admission (weave-npc at %s): %v", socket, err)
}
defer hresp.Body.Close()
out, _ := io.ReadAll(io.LimitReader(hresp.Body, 1<<16))
if hresp.StatusCode != http.StatusOK {
return resp, fmt.Errorf("egress admission refused (%s): %s", hresp.Status, strings.TrimSpace(string(out)))
}
if err := json.Unmarshal(out, &resp); err != nil {
return resp, fmt.Errorf("egress admission: bad answer: %v", err)
}
return resp, nil
}
+460
View File
@@ -0,0 +1,460 @@
package lb
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"sync"
"time"
coreapi "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"github.com/rajch/weave/common"
"github.com/rajch/weave/lb/admit"
)
// Controller watches Services, EndpointSlices, Nodes, EgressServices and this node's Pods, and keeps this
// node's part of the picture true. It also answers the CNI plugin's egress admissions.
type Controller struct {
self string
client kubernetes.Interface
egress rest.Interface
dp *Datapath
// readLocal is the node's own interfaces (swappable in tests)
readLocal func() (Local, error)
services, slices, nodes, pods, egresses cache.Store
synced []cache.InformerSynced
egressSynced cache.InformerSynced // nil while the EgressService API is absent
kick chan struct{}
ready chan struct{} // closed once the first pass ran
mu sync.Mutex // one pass at a time (the loop, or an admission)
lastProblems map[string]bool
// Services whose status this node wrote since it started (so a restart rewrites ipMode once)
written map[types.UID]string
// admitted: IP -> pod the CNI plugin gave it; fetched: pods admitted before the pod informer had them
admitted map[string]Admission
fetched map[string]*coreapi.Pod
at map[string]time.Time
// lastEgress is what was last applied, reused while the EgressService caches are not synced (never wipe
// members' rules on a partial view)
lastEgress EgressDesired
}
// Resync is how often the node re-reads its interfaces and re-applies everything even without API events.
const Resync = 30 * time.Second
// NewController runs nothing yet; Run does. egress is the EgressService API client.
func NewController(self string, client kubernetes.Interface, egress rest.Interface, dp *Datapath) *Controller {
return &Controller{self: self, client: client, egress: egress, dp: dp, readLocal: ReadLocal,
kick: make(chan struct{}, 1), ready: make(chan struct{}), lastProblems: map[string]bool{},
written: map[types.UID]string{}, admitted: map[string]Admission{}, fetched: map[string]*coreapi.Pod{},
at: map[string]time.Time{}}
}
func (c *Controller) poke() {
select {
case c.kick <- struct{}{}:
default:
}
}
func newInformer(c *Controller, lw cache.ListerWatcher, obj runtime.Object) (cache.Store, cache.Controller) {
return cache.NewInformer(lw, obj, 0, cache.ResourceEventHandlerFuncs{
AddFunc: func(interface{}) { c.poke() },
UpdateFunc: func(interface{}, interface{}) { c.poke() },
DeleteFunc: func(interface{}) { c.poke() },
})
}
// egressAPI reports whether the EgressService API is served (the CRD is installed).
func (c *Controller) egressAPI() (bool, error) {
_, err := c.client.Discovery().ServerResourcesForGroupVersion(GroupVersion.String())
if err == nil {
return true, nil
}
if apierrors.IsNotFound(err) {
return false, nil
}
return false, err
}
func (c *Controller) startEgress(ctx context.Context) {
var ctl cache.Controller
c.egresses, ctl = newInformer(c, cache.NewListWatchFromClient(c.egress, EgressServices, "", fields.Everything()), &EgressService{})
go ctl.Run(ctx.Done())
c.egressSynced = ctl.HasSynced
}
// Run blocks until ctx ends.
func (c *Controller) Run(ctx context.Context) {
var sc, ec, nc, pc cache.Controller
c.services, sc = newInformer(c, cache.NewListWatchFromClient(c.client.CoreV1().RESTClient(), "services", "", fields.Everything()), &coreapi.Service{})
c.slices, ec = newInformer(c, cache.NewListWatchFromClient(c.client.DiscoveryV1().RESTClient(), "endpointslices", "", fields.Everything()), &discovery.EndpointSlice{})
c.nodes, nc = newInformer(c, cache.NewListWatchFromClient(c.client.CoreV1().RESTClient(), "nodes", "", fields.Everything()), &coreapi.Node{})
c.pods, pc = newInformer(c, cache.NewListWatchFromClient(c.client.CoreV1().RESTClient(), "pods", "",
fields.OneTermEqualSelector("spec.nodeName", c.self)), &coreapi.Pod{})
for _, ctl := range []cache.Controller{sc, ec, nc, pc} {
go ctl.Run(ctx.Done())
c.synced = append(c.synced, ctl.HasSynced)
}
// with the API present, nothing is applied before EgressServices are known: an empty view would wipe
// members' rules (their traffic would leave from the node); without it, there can be no members
for {
present, err := c.egressAPI()
if err == nil {
if present {
c.startEgress(ctx)
c.synced = append(c.synced, c.egressSynced)
}
break
}
common.Log.Warnf("lb: cannot tell whether the EgressService API is served (retrying): %v", err)
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
}
}
go c.serveAdmission(ctx)
if !cache.WaitForCacheSync(ctx.Done(), c.synced...) {
return
}
tick := time.NewTicker(Resync)
defer tick.Stop()
c.poke()
first := true
for {
select {
case <-ctx.Done():
return
case <-tick.C:
if c.egressSynced == nil {
// the API may have been installed since
if present, err := c.egressAPI(); err == nil && present {
c.mu.Lock()
c.startEgress(ctx)
c.mu.Unlock()
}
}
case <-c.kick:
// let a burst of events settle into one pass
time.Sleep(200 * time.Millisecond)
select {
case <-c.kick:
default:
}
}
c.mu.Lock()
err := c.sync(ctx)
c.mu.Unlock()
if err != nil {
common.Log.Errorf("lb: %v", err)
}
if first {
first = false
close(c.ready)
}
}
}
// egressViewSynced: the EgressService view can be trusted (absent API: trivially empty; else synced).
func (c *Controller) egressViewSynced() bool {
return c.egressSynced == nil || c.egressSynced()
}
// sync is one pass: publish this node, compute, apply, write statuses. Callers hold c.mu.
func (c *Controller) sync(ctx context.Context) error {
local, err := c.readLocal()
if err != nil {
return fmt.Errorf("reading interfaces: %v", err)
}
if err := c.publish(ctx, local); err != nil {
return err
}
if local.PodNet == nil {
return fmt.Errorf("weave bridge has no address yet; not forwarding")
}
var nodes []Node
var problems []string
for _, o := range c.nodes.List() {
n, err := ParseNode(o.(*coreapi.Node))
if err != nil {
problems = append(problems, err.Error())
}
nodes = append(nodes, n)
}
var svcs []*coreapi.Service
for _, o := range c.services.List() {
svcs = append(svcs, o.(*coreapi.Service))
}
var slices []*discovery.EndpointSlice
for _, o := range c.slices.List() {
slices = append(slices, o.(*discovery.EndpointSlice))
}
d := Compute(c.self, nodes, svcs, slices)
// rewrite once after start what an earlier run may have written without ipMode
for _, f := range d.Forwards {
if s := c.service(f.Namespace, f.Name); s != nil && c.written[s.UID] != f.Address.String() && !hasStatusFor(d, f) {
d.Status = append(d.Status, Status{Namespace: f.Namespace, Name: f.Name, Address: f.Address})
}
}
e := c.lastEgress
e.Status = nil // reusing the datapath, never the writes of an earlier view
if c.egressViewSynced() {
e = c.computeEgress(nodes, svcs)
} else {
problems = append(problems, "EgressServices not synced yet: keeping the egress rules last applied")
}
current := map[string]bool{}
for _, p := range append(append(problems, d.Problems...), e.Problems...) {
current[p] = true
if !c.lastProblems[p] {
common.Log.Warnf("lb: %s", p)
}
}
c.lastProblems = current
if err := c.dp.Apply(d, e, c.self, local); err != nil {
return fmt.Errorf("applying datapath: %v", err)
}
c.lastEgress = e
for _, st := range d.Status {
if err := c.writeStatus(ctx, st); err != nil {
return err
}
}
for _, st := range e.Status {
if err := PatchEgressStatus(ctx, c.egress, st.Namespace, st.Name, st.Status); err != nil {
return fmt.Errorf("status of egress %s/%s: %v", st.Namespace, st.Name, err)
}
common.Log.Infof("lb: egress %s/%s: host %q, address %q, %d port blocks", st.Namespace, st.Name, st.Status.Host, st.Status.Address, len(st.Status.Blocks))
}
return nil
}
// computeEgress gathers this node's pods (informer, plus pods admitted before the informer had them) and computes.
func (c *Controller) computeEgress(nodes []Node, svcs []*coreapi.Service) EgressDesired {
var egresses []*EgressService
if c.egresses != nil {
for _, o := range c.egresses.List() {
egresses = append(egresses, o.(*EgressService))
}
}
seen := map[string]bool{}
var pods []*coreapi.Pod
for _, o := range c.pods.List() {
p := o.(*coreapi.Pod)
seen[p.Namespace+"/"+p.Name] = true
pods = append(pods, p)
}
// forget admissions the API has caught up with or that are long gone
for ip, a := range c.admitted {
key := a.Namespace + "/" + a.Name
o, ok, _ := c.pods.GetByKey(key)
caughtUp := ok && o.(*coreapi.Pod).Status.PodIP == ip
if caughtUp || (!ok && time.Since(c.at[ip]) > 2*time.Minute) {
delete(c.admitted, ip)
delete(c.at, ip)
}
}
for key, p := range c.fetched {
stillAdmitted := false
for _, a := range c.admitted {
if a.Namespace+"/"+a.Name == key {
stillAdmitted = true
}
}
if !stillAdmitted {
delete(c.fetched, key)
continue
}
if !seen[key] {
pods = append(pods, p)
}
}
return ComputeEgress(c.self, nodes, svcs, egresses, pods, c.admitted)
}
func hasStatusFor(d Desired, f Forward) bool {
for _, s := range d.Status {
if s.Namespace == f.Namespace && s.Name == f.Name {
return true
}
}
return false
}
func (c *Controller) service(ns, name string) *coreapi.Service {
o, ok, err := c.services.GetByKey(ns + "/" + name)
if err != nil || !ok {
return nil
}
return o.(*coreapi.Service)
}
// serveAdmission answers the CNI plugin on admit.SocketPath: record the pod's IP, run a pass, answer once the
// pod's rules are in place. A pod is refused (the plugin fails the ADD, kubelet retries) while this node cannot
// yet tell whether it is a member.
func (c *Controller) serveAdmission(ctx context.Context) {
if err := os.MkdirAll(filepath.Dir(admit.SocketPath), 0o700); err != nil {
common.Log.Errorf("lb: admission socket: %v", err)
return
}
_ = os.Remove(admit.SocketPath)
l, err := net.Listen("unix", admit.SocketPath)
if err != nil {
common.Log.Errorf("lb: admission socket: %v", err)
return
}
_ = os.Chmod(admit.SocketPath, 0o600)
mux := http.NewServeMux()
mux.HandleFunc("/admit", func(w http.ResponseWriter, r *http.Request) {
var req admit.Request
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil {
http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest)
return
}
member, code, err := c.admit(r.Context(), req)
if err != nil {
common.Log.Warnf("lb: admission of %s/%s (%s): %v", req.Namespace, req.Name, req.IP, err)
http.Error(w, err.Error(), code)
return
}
_ = json.NewEncoder(w).Encode(admit.Response{Member: member})
})
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second}
go func() {
<-ctx.Done()
_ = srv.Close()
}()
if err := srv.Serve(l); err != nil && err != http.ErrServerClosed {
common.Log.Errorf("lb: admission server: %v", err)
}
}
func (c *Controller) admit(ctx context.Context, req admit.Request) (bool, int, error) {
ip := net.ParseIP(req.IP).To4()
if req.Namespace == "" || req.Name == "" || ip == nil {
return false, http.StatusBadRequest, fmt.Errorf("need namespace, name and an IPv4 address")
}
select {
case <-c.ready:
case <-ctx.Done():
return false, http.StatusServiceUnavailable, ctx.Err()
case <-time.After(admit.Timeout - 5*time.Second):
return false, http.StatusServiceUnavailable, fmt.Errorf("not ready yet")
}
key := req.Namespace + "/" + req.Name
var pod *coreapi.Pod
if o, ok, _ := c.pods.GetByKey(key); ok {
pod = o.(*coreapi.Pod)
} else {
gctx, cancel := context.WithTimeout(ctx, 10*time.Second)
p, err := c.client.CoreV1().Pods(req.Namespace).Get(gctx, req.Name, metav1.GetOptions{})
cancel()
if err != nil {
return false, http.StatusServiceUnavailable, fmt.Errorf("fetching pod: %v", err)
}
pod = p
}
if pod.Spec.NodeName != c.self {
return false, http.StatusConflict, fmt.Errorf("pod is bound to %q, not this node", pod.Spec.NodeName)
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.egressViewSynced() {
return false, http.StatusServiceUnavailable, fmt.Errorf("EgressServices not synced yet")
}
c.admitted[ip.String()] = Admission{Namespace: req.Namespace, Name: req.Name}
c.at[ip.String()] = time.Now()
if _, ok, _ := c.pods.GetByKey(key); !ok {
c.fetched[key] = pod
}
if err := c.sync(ctx); err != nil {
// with no EgressService and no member rules in place there is nothing to protect or to clean up for
// this IP: a broken pass must not stop every pod on the node from starting
if len(c.lastEgress.Members) == 0 && (c.egresses == nil || len(c.egresses.List()) == 0) {
common.Log.Warnf("lb: admitting %s/%s despite a failed pass (no EgressServices): %v", req.Namespace, req.Name, err)
return false, http.StatusOK, nil
}
return false, http.StatusServiceUnavailable, err
}
for _, m := range c.lastEgress.Members {
if m.IP.Equal(ip) {
return true, http.StatusOK, nil
}
}
return false, http.StatusOK, nil
}
// publish keeps this node's annotations equal to what its interfaces say.
func (c *Controller) publish(ctx context.Context, local Local) error {
want := local.Annotations()
o, ok, err := c.nodes.GetByKey(c.self)
if err != nil {
return err
}
if ok {
have := o.(*coreapi.Node).Annotations
same := true
for k, v := range want {
if have[k] != v {
same = false
}
}
if same {
return nil
}
}
patch, err := json.Marshal(map[string]interface{}{"metadata": map[string]interface{}{"annotations": want}})
if err != nil {
return err
}
if _, err := c.client.CoreV1().Nodes().Patch(ctx, c.self, types.MergePatchType, patch, metav1.PatchOptions{}); err != nil {
return fmt.Errorf("publishing node annotations: %v", err)
}
common.Log.Infof("lb: node %s holds [%s]", c.self, want[AnnAddresses])
return nil
}
// writeStatus sets (ipMode Proxy: kube-proxy leaves the address to us) or clears a Service's load-balancer status.
func (c *Controller) writeStatus(ctx context.Context, st Status) error {
ingress := []map[string]string{}
if st.Address != nil {
ingress = append(ingress, map[string]string{"ip": st.Address.String(), "ipMode": "Proxy"})
}
patch, err := json.Marshal(map[string]interface{}{"status": map[string]interface{}{"loadBalancer": map[string]interface{}{"ingress": ingress}}})
if err != nil {
return err
}
s, err := c.client.CoreV1().Services(st.Namespace).Patch(ctx, st.Name, types.MergePatchType, patch, metav1.PatchOptions{}, "status")
if err != nil {
return fmt.Errorf("status of %s/%s: %v", st.Namespace, st.Name, err)
}
if st.Address != nil {
c.written[s.UID] = st.Address.String()
common.Log.Infof("lb: %s/%s at %s", st.Namespace, st.Name, st.Address)
} else {
delete(c.written, s.UID)
common.Log.Infof("lb: %s/%s has no address", st.Namespace, st.Name)
}
return nil
}
+336
View File
@@ -0,0 +1,336 @@
package lb
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"os/exec"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
const (
// ForwardChain (nat, from PREROUTING) DNATs held addresses' ports to endpoints.
ForwardChain = "REWEAVE-LB"
// PostChain (nat, from POSTROUTING, ahead of weave's WEAVE) keeps the client's address on what ForwardChain DNATed.
PostChain = "REWEAVE-LB-POST"
// MarkChain (mangle, from PREROUTING) marks holders' connections and steers their replies.
MarkChain = "REWEAVE-LB-MARK"
// servicePrefix names the per-port chains ForwardChain jumps to.
servicePrefix = "REWEAVE-LBS-"
markShift = 24
markMask = uint32(0xff) << markShift
// RulePriority is where the reply-steering rules sit: before the main table (32766).
RulePriority = 9000
// TableBase + mark is the routing table that sends replies to that mark's holder.
TableBase = 52000
bridge = "weave"
)
func markValue(m uint32) uint32 { return m << markShift }
// portChain is the per-(service, protocol, port) chain name: stable, short enough for iptables (28 chars).
func portChain(f Forward, p Port) string {
h := sha256.Sum256([]byte(fmt.Sprintf("%s/%s/%s/%d", f.Namespace, f.Name, p.Protocol, p.Port)))
return servicePrefix + strings.ToUpper(hex.EncodeToString(h[:])[:12])
}
// Rules is the full content of the chains this package owns, in iptables-restore form.
type Rules struct {
Nat, Mangle, Filter string
}
// Render turns the desired state (load-balancer forwards and egress) into iptables-restore input for the nat,
// mangle and filter tables. podNet is the weave allocation range (pods and bridges).
// Each payload declares every chain it owns, so a restore with --noflush replaces exactly those.
func Render(d Desired, e EgressDesired, self string, podNet *net.IPNet, stale []string) Rules {
var nat bytes.Buffer
nat.WriteString("*nat\n")
for _, c := range []string{ForwardChain, PostChain, ClassChain} {
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
}
var body bytes.Buffer
seen := map[string]bool{}
for _, f := range d.Forwards {
a := f.Address.String()
if seen[a] {
continue
}
seen[a] = true
// weave masquerades everything entering the pod network from outside unless it is for a local pod
// (its expose rule); a connection we DNATed must keep the client's address, so it leaves the nat
// table here. One from a pod (hairpin) is masqueraded to this node, so its reply comes back here.
fmt.Fprintf(&body, "-A %s -m conntrack --ctstate DNAT --ctorigdst %s/32 -s %s -j MASQUERADE\n", PostChain, a, podNet)
fmt.Fprintf(&body, "-A %s -m conntrack --ctstate DNAT --ctorigdst %s/32 -j ACCEPT\n", PostChain, a)
}
for _, f := range d.Forwards {
for _, p := range f.Ports {
c := portChain(f, p)
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
proto := strings.ToLower(string(p.Protocol))
fmt.Fprintf(&body, "-A %s -d %s/32 -p %s -m %s --dport %d -m comment --comment %q -j %s\n",
ForwardChain, f.Address, proto, proto, p.Port, f.Namespace+"/"+f.Name, c)
n := len(p.Endpoints)
for i, e := range p.Endpoints {
// DNAT to an ip:port needs the protocol in the same rule
if i < n-1 {
// the i-th endpoint takes 1/(n-i) of what the earlier ones left: uniform overall
fmt.Fprintf(&body, "-A %s -p %s -m statistic --mode random --probability %.10f -j DNAT --to-destination %s\n",
c, proto, 1/float64(n-i), e)
} else {
fmt.Fprintf(&body, "-A %s -p %s -j DNAT --to-destination %s\n", c, proto, e)
}
}
}
}
for _, c := range stale {
// chains of ports no longer forwarded: emptied here, deleted after the restore
fmt.Fprintf(&nat, ":%s - [0:0]\n", c)
}
var mangle bytes.Buffer
mangle.WriteString("*mangle\n")
for _, c := range []string{MarkChain, MSSChain} {
fmt.Fprintf(&mangle, ":%s - [0:0]\n", c)
}
if len(d.Holders) > 0 {
// replies of a holder's connection carry its mark: the policy rule routes them back to it
fmt.Fprintf(&mangle, "-A %s -m conntrack --ctdir REPLY -m connmark ! --mark 0x0/0x%x -j CONNMARK --restore-mark --nfmask 0x%x --ctmask 0x%x\n",
MarkChain, markMask, markMask, markMask)
for _, h := range d.Holders {
// a new connection to a pod here, arriving from the holder's bridge, from outside the pod network:
// the holder DNATed it and must see its replies
fmt.Fprintf(&mangle, "-A %s -i %s -m mac --mac-source %s ! -s %s -d %s -m conntrack --ctstate NEW -m comment --comment %q -j CONNMARK --set-xmark 0x%x/0x%x\n",
MarkChain, bridge, h.Node.WeaveMAC, podNet, podNet, "holder "+h.Node.Name, markValue(h.Mark), markMask)
}
}
var filter bytes.Buffer
filter.WriteString("*filter\n")
for _, c := range []string{FwdChain, InChain} {
fmt.Fprintf(&filter, ":%s - [0:0]\n", c)
}
renderEgress(e, self, podNet, &body, &mangle, &filter)
nat.Write(body.Bytes())
nat.WriteString("COMMIT\n")
mangle.WriteString("COMMIT\n")
filter.WriteString("COMMIT\n")
return Rules{Nat: nat.String(), Mangle: mangle.String(), Filter: filter.String()}
}
// Datapath applies Desired to this node.
type Datapath struct {
ipt *iptables.IPTables
// restore runs iptables-restore --noflush with the given input (swappable in tests)
restore func(input string) error
}
// NewDatapath uses the host's iptables (whichever backend the image's launch script selected).
func NewDatapath(ipt *iptables.IPTables) *Datapath {
return &Datapath{ipt: ipt, restore: func(input string) error {
cmd := exec.Command("iptables-restore", "--noflush", "-w", "5")
cmd.Stdin = strings.NewReader(input)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("iptables-restore: %v: %s", err, out)
}
return nil
}}
}
// Apply makes the node's iptables chains, tunnel, policy rules and routes exactly what d and e say.
func (dp *Datapath) Apply(d Desired, e EgressDesired, self string, local Local) error {
podNet := local.PodNet
if err := dp.hooks(); err != nil {
return err
}
// the tunnels must exist before the routes into them (iptables rules may name a device that does not exist yet)
var peers []Node
if e.Tunnelled(self) {
peers = e.Peers(self)
}
if err := ensureTunnels(peers, local); err != nil {
return err
}
existing, err := dp.ipt.ListChains("nat")
if err != nil {
return err
}
want := map[string]bool{}
for _, f := range d.Forwards {
for _, p := range f.Ports {
want[portChain(f, p)] = true
}
}
var stale []string
for _, c := range existing {
if strings.HasPrefix(c, servicePrefix) && !want[c] {
stale = append(stale, c)
}
}
r := Render(d, e, self, podNet, stale)
// filter first: the guards are in place before the marks and SNATs that they guard
for _, payload := range []string{r.Filter, r.Nat, r.Mangle} {
if err := dp.restore(payload); err != nil {
return err
}
}
for _, c := range stale {
if err := dp.ipt.DeleteChain("nat", c); err != nil {
return fmt.Errorf("deleting stale chain %s: %v", c, err)
}
}
if err := dp.routes(d.Holders); err != nil {
return err
}
return egressNet(e, self)
}
// hooks makes sure the built-in chains jump to ours: first (kube-proxy's and weave's rules must not see these
// connections first), except ClassChain, which must come after kube-proxy's KUBE-SERVICES (whose DNAT ends the
// nat walk, so a connection to a Service is never classed as going outside).
func (dp *Datapath) hooks() error {
for _, h := range []struct {
table, from, chain string
last bool
}{
{"nat", "PREROUTING", ForwardChain, false},
{"nat", "POSTROUTING", PostChain, false},
{"nat", "PREROUTING", ClassChain, true},
{"mangle", "PREROUTING", MarkChain, false},
{"mangle", "FORWARD", MSSChain, false},
{"filter", "FORWARD", FwdChain, false},
{"filter", "INPUT", InChain, false},
} {
ok, err := dp.ipt.ChainExists(h.table, h.chain)
if err != nil {
return err
}
if !ok {
if err := dp.ipt.NewChain(h.table, h.chain); err != nil {
return err
}
}
rule := []string{"-m", "comment", "--comment", "reweave lb", "-j", h.chain}
rules, err := dp.ipt.List(h.table, h.from)
if err != nil {
return err
}
if h.last {
ours, kube := -1, -1
for i, r := range rules {
if strings.HasSuffix(r, "-j "+h.chain) {
ours = i
}
if strings.HasSuffix(r, "-j KUBE-SERVICES") {
kube = i
}
}
if ours > kube {
continue
}
if err := dp.ipt.DeleteIfExists(h.table, h.from, rule...); err != nil {
return err
}
if err := dp.ipt.Append(h.table, h.from, rule...); err != nil {
return err
}
continue
}
// rules[0] is the policy line; our jump must be rules[1]
if len(rules) > 1 && strings.HasSuffix(rules[1], "-j "+h.chain) {
continue
}
if err := dp.ipt.DeleteIfExists(h.table, h.from, rule...); err != nil {
return err
}
if err := dp.ipt.Insert(h.table, h.from, 1, rule...); err != nil {
return err
}
}
return nil
}
// routes keeps one policy rule and one table per holder: marked replies go to the holder's bridge.
func (dp *Datapath) routes(holders []Holder) error {
link, err := netlink.LinkByName(bridge)
if err != nil {
if len(holders) == 0 {
return nil
}
return fmt.Errorf("weave bridge: %v", err)
}
want := map[int]Holder{}
for _, h := range holders {
want[TableBase+int(h.Mark)] = h
}
rules, err := netlink.RuleList(unix.AF_INET)
if err != nil {
return err
}
have := map[int]bool{}
for _, r := range rules {
if r.Priority != RulePriority || r.Table < TableBase || r.Table > TableBase+MaxMarks {
continue
}
h, ok := want[r.Table]
if ok && uint32(r.Mark) == markValue(h.Mark) && uint32(r.Mask) == markMask {
have[r.Table] = true
continue
}
r := r
if err := netlink.RuleDel(&r); err != nil {
return fmt.Errorf("removing stale rule to table %d: %v", r.Table, err)
}
}
existing, err := routesByTable(TableBase+1, TableBase+MaxMarks)
if err != nil {
return err
}
for table := TableBase + 1; table <= TableBase+MaxMarks; table++ {
h, ok := want[table]
if !ok {
for _, rt := range existing[table] {
if err := delRoute(rt); err != nil {
return fmt.Errorf("removing stale route in table %d: %v", table, err)
}
}
continue
}
if err := netlink.RouteReplace(&netlink.Route{
LinkIndex: link.Attrs().Index,
Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
Gw: h.Node.WeaveIP,
Table: table,
Flags: int(netlink.FLAG_ONLINK),
}); err != nil {
return fmt.Errorf("route to holder %s: %v", h.Node.Name, err)
}
if !have[table] {
rule := netlink.NewRule()
rule.Priority = RulePriority
rule.Table = table
rule.Mark = int(markValue(h.Mark))
rule.Mask = int(markMask)
if err := netlink.RuleAdd(rule); err != nil {
return fmt.Errorf("rule for holder %s: %v", h.Node.Name, err)
}
}
}
return nil
}
// delRoute deletes a route as RouteList returned it: a default route lists without Dst, which RouteDel refuses.
func delRoute(rt netlink.Route) error {
if rt.Dst == nil {
rt.Dst = &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
}
return netlink.RouteDel(&rt)
}
+340
View File
@@ -0,0 +1,340 @@
package lb
import (
"fmt"
"net"
"sort"
"strings"
coreapi "k8s.io/api/core/v1"
)
// Port blocks: every node gets its own source ports on an EgressService's address, so the holder can route a
// reply back to the member's node by port alone (no state on the holder). The range lies above the default
// ephemeral range (32768-60999), so it never meets the holder's own connections from the address.
const (
BlockBase = 61000
BlockSize = 512
BlockCount = 8 // 61000-65095
)
// Admission is a pod the CNI plugin announced at ADD, before the API shows its IP.
type Admission struct {
Namespace, Name string
}
// Member is one pod on this node that an EgressService selects, and where its outside traffic goes.
type Member struct {
IP net.IP
Pod string // namespace/name
Service string // namespace/name of its EgressService (empty if it is in several)
// Unreachable: its outside traffic is dropped (no holder, no block, several EgressServices, ...)
Unreachable bool
Address net.IP // it leaves from Address:[From, To]
From, To int32
Holder Node // the node that emits it; Holder.Name == self: emitted here
}
// BlockRoute is, on the holder, the replies to Address:[From, To] going back to Peer.
type BlockRoute struct {
Address net.IP
From, To int32
Peer Node
}
// EgressStatusWrite is a status this node writes (the holder, or the first node by name to clear it).
type EgressStatusWrite struct {
Namespace, Name string
Status EgressServiceStatus
}
// EgressDesired is this node's part of every EgressService.
type EgressDesired struct {
Members []Member
Routes []BlockRoute
Status []EgressStatusWrite
NodeAddrs []net.IP // the cluster's node addresses: members' traffic to them is not "outside"
Problems []string
}
// Tunnelled reports whether this node sends or receives anything over the egress tunnel.
func (e EgressDesired) Tunnelled(self string) bool {
if len(e.Routes) > 0 {
return true
}
for _, m := range e.Members {
if !m.Unreachable && m.Holder.Name != self {
return true
}
}
return false
}
// Peers are the nodes this node exchanges tunnelled packets with (sorted by name, deduplicated).
func (e EgressDesired) Peers(self string) []Node {
seen := map[string]Node{}
for _, r := range e.Routes {
seen[r.Peer.Name] = r.Peer
}
for _, m := range e.Members {
if !m.Unreachable && m.Holder.Name != self {
seen[m.Holder.Name] = m.Holder
}
}
out := make([]Node, 0, len(seen))
for _, n := range seen {
out = append(out, n)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// AllocateBlocks keeps every valid block a node already has and gives each node without one the lowest free
// block, in name order. A node beyond BlockCount gets none (its members are dropped, and that is reported).
func AllocateBlocks(current []PortBlock, nodeNames []string) []PortBlock {
present := map[string]bool{}
for _, n := range nodeNames {
present[n] = true
}
used := map[int]bool{}
have := map[string]PortBlock{}
for _, b := range current {
k := int(b.From-BlockBase) / BlockSize
valid := present[b.Node] && b.From >= BlockBase && int(b.From-BlockBase)%BlockSize == 0 &&
k < BlockCount && b.To == b.From+BlockSize-1
if !valid || used[k] {
continue
}
if _, dup := have[b.Node]; dup {
continue
}
used[k] = true
have[b.Node] = b
}
names := append([]string(nil), nodeNames...)
sort.Strings(names)
for _, n := range names {
if _, ok := have[n]; ok {
continue
}
for k := 0; k < BlockCount; k++ {
if !used[k] {
used[k] = true
from := int32(BlockBase + k*BlockSize)
have[n] = PortBlock{Node: n, From: from, To: from + BlockSize - 1}
break
}
}
}
out := make([]PortBlock, 0, len(have))
for _, b := range have {
out = append(out, b)
}
sort.Slice(out, func(i, j int) bool { return out[i].Node < out[j].Node })
return out
}
func sameStatus(a, b EgressServiceStatus) bool {
if a.Host != b.Host || a.Address != b.Address || len(a.Blocks) != len(b.Blocks) {
return false
}
for i := range a.Blocks {
if a.Blocks[i] != b.Blocks[i] {
return false
}
}
return true
}
// holderOf is S1's rule for a Service's address: the one node that holds spec.loadBalancerIP.
func holderOf(s *coreapi.Service, nodes []Node) (*Node, net.IP) {
want := net.ParseIP(s.Spec.LoadBalancerIP).To4()
if want == nil {
return nil, nil
}
var hs []Node
for _, n := range nodes {
if n.Holds(want) {
hs = append(hs, n)
}
}
if len(hs) != 1 {
return nil, want
}
return &hs[0], want
}
// selects reports whether a Service's selector (non-empty) matches the labels.
func selects(s *coreapi.Service, labels map[string]string) bool {
if len(s.Spec.Selector) == 0 {
return false
}
for k, v := range s.Spec.Selector {
if labels[k] != v {
return false
}
}
return true
}
// ComputeEgress decides this node's part of every EgressService: the statuses it writes, the members it hosts and
// where their traffic goes, and (as a holder) where replies to each node's port block go.
// pods are this node's pods; admitted maps an IP to the pod the CNI plugin gave it (newer than the API may show).
func ComputeEgress(self string, nodes []Node, services []*coreapi.Service, egresses []*EgressService,
pods []*coreapi.Pod, admitted map[string]Admission) EgressDesired {
var d EgressDesired
sorted := append([]Node(nil), nodes...)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Name < sorted[j].Name })
byName := map[string]Node{}
var names []string
for _, n := range sorted {
byName[n.Name] = n
names = append(names, n.Name)
d.NodeAddrs = append(d.NodeAddrs, n.Addresses...)
}
svcs := map[string]*coreapi.Service{}
for _, s := range services {
svcs[s.Namespace+"/"+s.Name] = s
}
es := append([]*EgressService(nil), egresses...)
sort.Slice(es, func(i, j int) bool { return es[i].Namespace+"/"+es[i].Name < es[j].Namespace+"/"+es[j].Name })
// valid[key]: the EgressService is in force: its Service is served from an address one node holds, and the
// holder has published that (status) - what members and holder both act on
valid := map[string]bool{}
for _, e := range es {
key := e.Namespace + "/" + e.Name
var want EgressServiceStatus
var holder *Node
var addr net.IP
s := svcs[key]
switch {
case e.Spec.Emit != EmitHolder:
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: spec.emit must be %q (the only mode so far), got %q", key, EmitHolder, e.Spec.Emit))
case e.Spec.SourceIPBy != "" && e.Spec.SourceIPBy != SourceIPByLoadBalancerIP:
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: spec.sourceIPBy must be %q, got %q", key, SourceIPByLoadBalancerIP, e.Spec.SourceIPBy))
case s == nil:
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: no Service %s", key, key))
case !eligible(s):
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: Service %s is not a LoadBalancer of class %s", key, key, Class))
default:
holder, addr = holderOf(s, sorted)
if holder == nil {
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: no single node holds %s; members' outside traffic is dropped", key, s.Spec.LoadBalancerIP))
}
}
writer := ""
if holder != nil {
want = EgressServiceStatus{Host: holder.Name, Address: addr.String(), Blocks: AllocateBlocks(e.Status.Blocks, names)}
writer = holder.Name
if len(want.Blocks) < len(names) {
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: only %d port blocks for %d nodes; members on nodes without one are dropped", key, BlockCount, len(names)))
}
} else if len(names) > 0 {
writer = names[0]
}
if writer == self && !sameStatus(e.Status, want) {
d.Status = append(d.Status, EgressStatusWrite{Namespace: e.Namespace, Name: e.Name, Status: want})
}
// in force only once the status in the API says what holds now (members and holder read the same thing)
if holder != nil && e.Status.Host == holder.Name && e.Status.Address == addr.String() {
valid[key] = true
if holder.Name == self {
for _, b := range e.Status.Blocks {
if b.Node == self {
continue
}
peer, ok := byName[b.Node]
if !ok || peer.WeaveIP == nil {
d.Problems = append(d.Problems, fmt.Sprintf("egress %s: node %s has no weave bridge published; its members' replies cannot reach it", key, b.Node))
continue
}
d.Routes = append(d.Routes, BlockRoute{Address: addr, From: b.From, To: b.To, Peer: peer})
}
}
}
}
// this node's pods, one per IP: the CNI plugin's latest word wins, then live pods over terminating ones
type candidate struct {
pod *coreapi.Pod
ip string
}
byIP := map[string]*coreapi.Pod{}
podByKey := map[string]*coreapi.Pod{}
for _, p := range pods {
podByKey[p.Namespace+"/"+p.Name] = p
}
var cands []candidate
for _, p := range pods {
if p.Spec.NodeName != self || p.Spec.HostNetwork || p.Status.Phase == coreapi.PodSucceeded || p.Status.Phase == coreapi.PodFailed {
continue
}
if ip := net.ParseIP(p.Status.PodIP).To4(); ip != nil {
cands = append(cands, candidate{p, ip.String()})
}
}
for ip, a := range admitted {
if p := podByKey[a.Namespace+"/"+a.Name]; p != nil && p.Spec.NodeName == self && !p.Spec.HostNetwork {
cands = append(cands, candidate{p, ip})
}
}
for _, c := range cands {
if a, ok := admitted[c.ip]; ok && (a.Namespace != c.pod.Namespace || a.Name != c.pod.Name) {
continue // the IP was given to another pod since
}
cur := byIP[c.ip]
if cur == nil || (cur.DeletionTimestamp != nil && c.pod.DeletionTimestamp == nil) ||
(cur.DeletionTimestamp == nil) == (c.pod.DeletionTimestamp == nil) && c.pod.CreationTimestamp.After(cur.CreationTimestamp.Time) {
byIP[c.ip] = c.pod
}
}
ips := make([]string, 0, len(byIP))
for ip := range byIP {
ips = append(ips, ip)
}
sort.Strings(ips)
for _, ip := range ips {
p := byIP[ip]
var in []*EgressService
for _, e := range es {
if e.Namespace != p.Namespace {
continue
}
if s := svcs[e.Namespace+"/"+e.Name]; s != nil && selects(s, p.Labels) {
in = append(in, e)
}
}
if len(in) == 0 {
continue
}
m := Member{IP: net.ParseIP(ip).To4(), Pod: p.Namespace + "/" + p.Name, Unreachable: true}
if len(in) > 1 {
keys := make([]string, len(in))
for i, e := range in {
keys[i] = e.Namespace + "/" + e.Name
}
d.Problems = append(d.Problems, fmt.Sprintf("pod %s is selected by several EgressServices (%s); its outside traffic is dropped", m.Pod, strings.Join(keys, ", ")))
d.Members = append(d.Members, m)
continue
}
e := in[0]
m.Service = e.Namespace + "/" + e.Name
if valid[m.Service] {
holder := byName[e.Status.Host]
for _, b := range e.Status.Blocks {
if b.Node == self && (holder.Name == self || holder.WeaveIP != nil) {
m.Unreachable = false
m.Address = net.ParseIP(e.Status.Address).To4()
m.From, m.To = b.From, b.To
m.Holder = holder
}
}
if m.Unreachable {
d.Problems = append(d.Problems, fmt.Sprintf("pod %s: egress %s has no port block for %s (or its holder's bridge is unknown); its outside traffic is dropped", m.Pod, m.Service, self))
}
}
d.Members = append(d.Members, m)
}
return d
}
+144
View File
@@ -0,0 +1,144 @@
package lb
import (
"context"
"encoding/json"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
)
// EgressService (reweave.levk.dev/v1alpha1) makes the pods a LoadBalancer Service selects leave the cluster
// from the Service's address. It is named after the Service (same namespace, same name). Its semantics follow
// OVN-Kubernetes' EgressService (k8s.ovn.org/v1, sourceIPBy: LoadBalancerIP) where they agree; they differ in
// that membership is every pod the Service's selector matches (by pod IP, never by readiness), the holder is
// the node that holds the address (no election), and a member's traffic to the outside leaves as the address
// or not at all.
type EgressService struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec EgressServiceSpec `json:"spec"`
Status EgressServiceStatus `json:"status,omitempty"`
}
// EgressServiceSpec is what the operator asks for.
type EgressServiceSpec struct {
// SourceIPBy is LoadBalancerIP: members leave from the Service's load-balancer address (the only mode in v1).
SourceIPBy string `json:"sourceIPBy,omitempty"`
// Emit is where members' packets leave the cluster. Explicit, no default: Holder (tunnelled to the node
// holding the address, which emits them) is the only mode in v1.
Emit string `json:"emit"`
}
// EgressServiceStatus is written by the holder alone.
type EgressServiceStatus struct {
// Host is the node that holds the address and emits members' packets.
Host string `json:"host,omitempty"`
// Address is the address members leave from.
Address string `json:"address,omitempty"`
// Blocks gives every node its own source ports on Address, so the holder routes replies back by port.
Blocks []PortBlock `json:"blocks,omitempty"`
}
// PortBlock is the source-port range [From, To] on the address that one node's members use.
type PortBlock struct {
Node string `json:"node"`
From int32 `json:"from"`
To int32 `json:"to"`
}
// EgressServiceList is a list of EgressServices.
type EgressServiceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []EgressService `json:"items"`
}
const (
// Group and Version of the EgressService API.
Group = "reweave.levk.dev"
Version = "v1alpha1"
// EgressServices is the resource name.
EgressServices = "egressservices"
SourceIPByLoadBalancerIP = "LoadBalancerIP"
EmitHolder = "Holder"
)
// GroupVersion of the EgressService API.
var GroupVersion = schema.GroupVersion{Group: Group, Version: Version}
// DeepCopyObject implements runtime.Object.
func (e *EgressService) DeepCopyObject() runtime.Object { return e.DeepCopy() }
// DeepCopy copies the whole object.
func (e *EgressService) DeepCopy() *EgressService {
if e == nil {
return nil
}
out := *e
e.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Status.Blocks = append([]PortBlock(nil), e.Status.Blocks...)
return &out
}
// DeepCopyObject implements runtime.Object.
func (l *EgressServiceList) DeepCopyObject() runtime.Object {
if l == nil {
return nil
}
out := *l
l.ListMeta.DeepCopyInto(&out.ListMeta)
out.Items = make([]EgressService, len(l.Items))
for i := range l.Items {
out.Items[i] = *l.Items[i].DeepCopy()
}
return &out
}
func addKnownTypes(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion, &EgressService{}, &EgressServiceList{})
metav1.AddToGroupVersion(s, GroupVersion)
return nil
}
// NewEgressClient is a REST client for the EgressService API (no generated clientset: two plain types).
func NewEgressClient(base *rest.Config) (*rest.RESTClient, error) {
scheme := runtime.NewScheme()
if err := addKnownTypes(scheme); err != nil {
return nil, err
}
cfg := *base
cfg.GroupVersion = &GroupVersion
cfg.APIPath = "/apis"
cfg.ContentType = runtime.ContentTypeJSON
cfg.NegotiatedSerializer = serializer.NewCodecFactory(scheme).WithoutConversion()
if cfg.UserAgent == "" {
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
}
return rest.RESTClientFor(&cfg)
}
// PatchEgressStatus merge-patches an EgressService's status (nil fields are removed).
func PatchEgressStatus(ctx context.Context, c rest.Interface, namespace, name string, st EgressServiceStatus) error {
body := map[string]interface{}{"host": nil, "address": nil, "blocks": nil}
if st.Host != "" {
body["host"] = st.Host
}
if st.Address != "" {
body["address"] = st.Address
}
if len(st.Blocks) > 0 {
body["blocks"] = st.Blocks
}
patch, err := json.Marshal(map[string]interface{}{"status": body})
if err != nil {
return err
}
return c.Patch(types.MergePatchType).Namespace(namespace).Resource(EgressServices).Name(name).
SubResource("status").Body(patch).Do(ctx).Error()
}
+530
View File
@@ -0,0 +1,530 @@
package lb
import (
"bytes"
"fmt"
"hash/fnv"
"net"
"os"
"sort"
"strings"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
const (
// ClassChain (nat, appended to PREROUTING after kube-proxy's KUBE-SERVICES) marks members' new connections to
// the outside. kube-proxy's DNAT ends the nat walk, so a connection to a Service never gets here.
ClassChain = "REWEAVE-LB-CLASS"
// FwdChain (filter, first in FORWARD) drops members' outside packets that would not leave as their address.
FwdChain = "REWEAVE-LB-FWD"
// InChain (filter, first in INPUT) accepts the egress tunnels only from the cluster's nodes.
InChain = "REWEAVE-LB-IN"
// MSSChain (mangle, first in FORWARD) clamps TCP MSS into the tunnels.
MSSChain = "REWEAVE-LB-MSS"
// TunnelPrefix names the egress tunnels: one ipip device per peer node, between the two nodes' weave bridges,
// so a holder knows which node a member's packet came from by the device it arrives on.
TunnelPrefix = "rweg-"
// tunnelGlob matches every egress tunnel in an iptables rule.
tunnelGlob = "rweg+"
// legacyTunnel is the single shared tunnel of earlier builds (removed when found).
legacyTunnel = "reweave-eg"
// EgressMark marks a member's packets to the outside (connmark on its connections, fwmark on each packet).
EgressMark = uint32(0x100000)
// peerFlag, with a peer's index in the top byte, tags (on the holder) a connection that arrived from that peer's
// tunnel: replies and related ICMP errors carry it back there.
peerFlag = uint32(0x200000)
peerMask = markMask | peerFlag
// HolderRulePriority: replies to a block go to its node; ahead of the local table, which moves to LocalRulePriority,
// since the address is usually local on the holder.
HolderRulePriority = 90
LocalRulePriority = 100
// MemberRulePriority: a member's marked packets go to its holder (or nowhere).
MemberRulePriority = 9100
// EgressTableBase + peer index routes into that peer's tunnel; EgressTableBase itself is unreachable.
EgressTableBase = 53000
ipipOverhead = 20
)
// portProtocols carry ports: their replies are routed by port block alone (stateless), and members' SNAT keeps
// within the node's block. Everything else is routed back by the tag the holder put on the connection.
var portProtocols = []struct {
name string
num int
}{{"tcp", unix.IPPROTO_TCP}, {"udp", unix.IPPROTO_UDP}, {"sctp", unix.IPPROTO_SCTP}}
// TunnelName is the egress tunnel towards a peer node (stable, within the 15 characters a device name may have).
func TunnelName(peer string) string {
h := fnv.New32a()
h.Write([]byte(peer))
return fmt.Sprintf("%s%08x", TunnelPrefix, h.Sum32())
}
func peerTag(idx uint32) uint32 { return markValue(idx) | peerFlag }
// renderEgress appends the egress rules of each table to the bodies of S1's payloads.
func renderEgress(e EgressDesired, self string, podNet *net.IPNet, nat, mangle, filter *bytes.Buffer) {
members := e.Members
if len(members) > 0 {
// what is not "outside": the pod network, multicast, the cluster's nodes
fmt.Fprintf(nat, "-A %s -d %s -j RETURN\n", ClassChain, podNet)
fmt.Fprintf(nat, "-A %s -d 224.0.0.0/4 -j RETURN\n", ClassChain)
seen := map[string]bool{}
for _, a := range e.NodeAddrs {
if !seen[a.String()] {
seen[a.String()] = true
fmt.Fprintf(nat, "-A %s -d %s/32 -j RETURN\n", ClassChain, a)
}
}
for _, m := range members {
fmt.Fprintf(nat, "-A %s -s %s/32 -m comment --comment %q -j CONNMARK --set-xmark 0x%x/0x%x\n", ClassChain, m.IP, m.Pod, EgressMark, EgressMark)
fmt.Fprintf(nat, "-A %s -s %s/32 -j MARK --set-xmark 0x%x/0x%x\n", ClassChain, m.IP, EgressMark, EgressMark)
}
// later packets of a member's outside connection carry the mark too (the nat table saw only the first)
fmt.Fprintf(mangle, "-A %s -m conntrack --ctdir ORIGINAL -m connmark --mark 0x%x/0x%x -j MARK --set-xmark 0x%x/0x%x\n",
MarkChain, EgressMark, EgressMark, EgressMark, EgressMark)
}
for _, m := range members {
if m.Unreachable {
fmt.Fprintf(filter, "-A %s -s %s/32 -m mark --mark 0x%x/0x%x -m comment --comment %q -j DROP\n", FwdChain, m.IP, EgressMark, EgressMark, m.Pod)
continue
}
out := "-o " + TunnelName(m.Holder.Name)
if m.Holder.Name == self {
// emitted here, from the address: never into the pod network unSNATed
out = "! -o " + bridge
fmt.Fprintf(filter, "-A %s -s %s/32 -o %s -m mark --mark 0x%x/0x%x -j DROP\n", FwdChain, m.IP, bridge, EgressMark, EgressMark)
}
// within this node's block for what has ports; the address alone for the rest (the holder tells those
// apart by the tunnel they arrive on)
for _, p := range portProtocols {
fmt.Fprintf(nat, "-A %s -s %s/32 %s -m mark --mark 0x%x/0x%x -p %s -j SNAT --to-source %s:%d-%d\n",
PostChain, m.IP, out, EgressMark, EgressMark, p.name, m.Address, m.From, m.To)
}
// ICMP queries (ping) too: NAT treats the echo identifier as a port, and without a block of their own two
// nodes' pods (whose small, alike PIDs make alike identifiers) collide on the holder
fmt.Fprintf(nat, "-A %s -s %s/32 %s -m mark --mark 0x%x/0x%x -p icmp -j SNAT --to-source %s:%d-%d\n",
PostChain, m.IP, out, EgressMark, EgressMark, m.Address, m.From, m.To)
fmt.Fprintf(nat, "-A %s -s %s/32 %s -m mark --mark 0x%x/0x%x -j SNAT --to-source %s\n",
PostChain, m.IP, out, EgressMark, EgressMark, m.Address)
fmt.Fprintf(filter, "-A %s -s %s/32 %s -m mark --mark 0x%x/0x%x -j RETURN\n", FwdChain, m.IP, out, EgressMark, EgressMark)
// a member's outside traffic leaves no other way
fmt.Fprintf(filter, "-A %s -s %s/32 -m mark --mark 0x%x/0x%x -m comment --comment %q -j DROP\n", FwdChain, m.IP, EgressMark, EgressMark, m.Pod)
}
if !e.Tunnelled(self) {
return
}
tags := peerIndex(e, self)
// as holder: tag each connection arriving from a peer's tunnel with that peer, once; replies and related ICMP
// errors restore the tag, and the policy rule for it sends them back into that tunnel
byPeer := map[string][]BlockRoute{}
var peerNames []string
for _, r := range e.Routes {
if _, ok := byPeer[r.Peer.Name]; !ok {
peerNames = append(peerNames, r.Peer.Name)
}
byPeer[r.Peer.Name] = append(byPeer[r.Peer.Name], r)
}
sort.Strings(peerNames)
if len(peerNames) > 0 {
fmt.Fprintf(mangle, "-A %s -m conntrack --ctdir REPLY -m connmark --mark 0x%x/0x%x -j CONNMARK --restore-mark --nfmask 0x%x --ctmask 0x%x\n",
MarkChain, peerFlag, peerFlag, peerMask, peerMask)
}
for _, name := range peerNames {
dev, tag := TunnelName(name), peerTag(tags[name])
fmt.Fprintf(mangle, "-A %s -i %s -m conntrack --ctstate NEW -m connmark --mark 0x0/0x%x -m comment --comment %q -j CONNMARK --set-xmark 0x%x/0x%x\n",
MarkChain, dev, peerMask, "from "+name, tag, peerMask)
// a connection another node already holds (a port-less protocol to the same remote): never cross-delivered
fmt.Fprintf(filter, "-A %s -i %s -m conntrack --ctdir ORIGINAL -m connmark ! --mark 0x%x/0x%x -j DROP\n", FwdChain, dev, tag, peerMask)
addrs := map[string]bool{}
for _, r := range byPeer[name] {
for _, p := range portProtocols {
fmt.Fprintf(filter, "-A %s -i %s -m conntrack --ctdir ORIGINAL -s %s/32 -p %s -m %s --sport %d:%d -m comment --comment %q -j RETURN\n",
FwdChain, dev, r.Address, p.name, p.name, r.From, r.To, "block of "+name)
}
addrs[r.Address.String()] = true
}
for _, a := range sortedSet(addrs) {
// outside its own block a node may not use the ports of others
for _, p := range portProtocols {
fmt.Fprintf(filter, "-A %s -i %s -m conntrack --ctdir ORIGINAL -s %s/32 -p %s -j DROP\n", FwdChain, dev, a, p.name)
}
fmt.Fprintf(filter, "-A %s -i %s -m conntrack --ctdir ORIGINAL -s %s/32 -j RETURN\n", FwdChain, dev, a)
}
}
// as a member's node: replies (and related errors) from the holder; nothing else comes back
addrs := map[string]bool{}
for _, m := range members {
if !m.Unreachable && m.Holder.Name != self {
addrs[m.Address.String()] = true
}
}
for _, a := range sortedSet(addrs) {
// still addressed to the address after nat: no member connection matched it
fmt.Fprintf(filter, "-A %s -i %s -d %s/32 -j DROP\n", FwdChain, tunnelGlob, a)
}
fmt.Fprintf(filter, "-A %s -i %s -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN\n", FwdChain, tunnelGlob)
fmt.Fprintf(filter, "-A %s -i %s -j DROP\n", FwdChain, tunnelGlob)
// the tunnels themselves only from the cluster's nodes' bridges
for _, p := range e.Peers(self) {
fmt.Fprintf(filter, "-A %s -p 4 -s %s/32 -m mac --mac-source %s -j RETURN\n", InChain, p.WeaveIP, p.WeaveMAC)
}
fmt.Fprintf(filter, "-A %s -p 4 -j DROP\n", InChain)
fmt.Fprintf(mangle, "-A %s -o %s -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n", MSSChain, tunnelGlob)
}
func sortedSet(m map[string]bool) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
// peerIndex gives each tunnel peer a stable index (1..MaxMarks): its tag and its routing table.
func peerIndex(e EgressDesired, self string) map[string]uint32 {
out := map[string]uint32{}
for _, h := range assignMarks(e.Peers(self)) {
out[h.Node.Name] = h.Mark
}
return out
}
// peerTables gives each tunnel peer its routing table (EgressTableBase + its index).
func peerTables(e EgressDesired, self string) map[string]int {
out := map[string]int{}
for name, idx := range peerIndex(e, self) {
out[name] = EgressTableBase + int(idx)
}
return out
}
// egressRule is a policy rule this package owns, in a comparable form.
type egressRule struct {
priority int
src, dst string
ipproto int
from, to uint16
mark, mask uint32
table int
}
func (r egressRule) key() string {
return fmt.Sprintf("%d|%s|%s|%d|%d-%d|%x/%x|%d", r.priority, r.src, r.dst, r.ipproto, r.from, r.to, r.mark, r.mask, r.table)
}
// wantRules is every egress policy rule this node needs.
func wantRules(e EgressDesired, self string) []egressRule {
tables := peerTables(e, self)
tags := peerIndex(e, self)
var out []egressRule
tagged := map[string]bool{}
for _, r := range e.Routes {
for _, p := range portProtocols {
out = append(out, egressRule{priority: HolderRulePriority, dst: r.Address.String() + "/32", ipproto: p.num,
from: uint16(r.From), to: uint16(r.To), table: tables[r.Peer.Name]})
}
if !tagged[r.Peer.Name] {
// whatever the tag says came from that peer (replies without ports, ICMP errors) goes back to it
tagged[r.Peer.Name] = true
out = append(out, egressRule{priority: HolderRulePriority, mark: peerTag(tags[r.Peer.Name]), mask: peerMask, table: tables[r.Peer.Name]})
}
}
for _, m := range e.Members {
if !m.Unreachable && m.Holder.Name == self {
continue // emitted here: the main table already sends it out
}
table := EgressTableBase
if !m.Unreachable {
table = tables[m.Holder.Name]
}
out = append(out, egressRule{priority: MemberRulePriority, src: m.IP.String() + "/32", mark: EgressMark, mask: EgressMark, table: table})
}
sort.Slice(out, func(i, j int) bool { return out[i].key() < out[j].key() })
return out
}
func ruleOf(r netlink.Rule) egressRule {
er := egressRule{priority: r.Priority, ipproto: r.IPProto, table: r.Table}
if r.Src != nil {
er.src = r.Src.String()
}
if r.Dst != nil {
er.dst = r.Dst.String()
}
if r.Dport != nil {
er.from, er.to = r.Dport.Start, r.Dport.End
}
if r.Mark > 0 {
er.mark = uint32(r.Mark)
er.mask = uint32(r.Mask)
}
if er.ipproto < 0 {
er.ipproto = 0
}
return er
}
// routesByTable lists, in one dump, the IPv4 routes of tables lo..hi.
func routesByTable(lo, hi int) (map[int][]netlink.Route, error) {
all, err := netlink.RouteListFiltered(unix.AF_INET, &netlink.Route{Table: unix.RT_TABLE_UNSPEC}, netlink.RT_FILTER_TABLE)
if err != nil {
return nil, err
}
out := map[int][]netlink.Route{}
for _, rt := range all {
if rt.Table >= lo && rt.Table <= hi {
out[rt.Table] = append(out[rt.Table], rt)
}
}
return out, nil
}
// egressNet makes the local rule's place, the policy rules and the tunnel tables exactly what e needs (the
// tunnels themselves are made first, by Apply).
func egressNet(e EgressDesired, self string) error {
if len(e.Routes) > 0 {
if err := moveLocalRule(); err != nil {
return err
}
}
tables := peerTables(e, self)
peerOf := map[int]Node{}
for _, p := range e.Peers(self) {
peerOf[tables[p.Name]] = p
}
unreachable := false
for _, m := range e.Members {
unreachable = unreachable || m.Unreachable
}
existing, err := routesByTable(EgressTableBase, EgressTableBase+MaxMarks)
if err != nil {
return err
}
for table := EgressTableBase; table <= EgressTableBase+MaxMarks; table++ {
var want *netlink.Route
if p, ok := peerOf[table]; ok {
link, err := netlink.LinkByName(TunnelName(p.Name))
if err != nil {
return fmt.Errorf("egress tunnel to %s: %v", p.Name, err)
}
want = &netlink.Route{LinkIndex: link.Attrs().Index, Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
Scope: netlink.SCOPE_LINK, Table: table}
} else if table == EgressTableBase && unreachable {
want = &netlink.Route{Dst: &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}, Table: table, Type: unix.RTN_UNREACHABLE}
}
keep := false
for _, rt := range existing[table] {
if want != nil && rt.Type == want.Type &&
(want.Type == unix.RTN_UNREACHABLE || rt.LinkIndex == want.LinkIndex && rt.Gw == nil) {
keep = true
continue
}
if err := delRoute(rt); err != nil {
return fmt.Errorf("removing stale route in table %d: %v", table, err)
}
}
if want != nil && !keep {
if err := netlink.RouteReplace(want); err != nil {
return fmt.Errorf("route in table %d: %v", table, err)
}
}
}
want := map[string]egressRule{}
for _, r := range wantRules(e, self) {
want[r.key()] = r
}
rules, err := netlink.RuleList(unix.AF_INET)
if err != nil {
return err
}
have := map[string]bool{}
for _, r := range rules {
if r.Priority != HolderRulePriority && r.Priority != MemberRulePriority {
continue
}
k := ruleOf(r).key()
if _, ok := want[k]; ok && !have[k] {
have[k] = true
continue
}
r := r
if err := netlink.RuleDel(&r); err != nil {
return fmt.Errorf("removing stale egress rule: %v", err)
}
}
for k, r := range want {
if have[k] {
continue
}
rule := netlink.NewRule()
rule.Priority = r.priority
rule.Table = r.table
if r.src != "" {
_, rule.Src, _ = net.ParseCIDR(r.src)
}
if r.dst != "" {
_, rule.Dst, _ = net.ParseCIDR(r.dst)
}
if r.ipproto != 0 {
rule.IPProto = r.ipproto
rule.Dport = netlink.NewRulePortRange(r.from, r.to)
}
if r.mark != 0 {
rule.Mark = int(r.mark)
rule.Mask = int(r.mask)
}
if err := netlink.RuleAdd(rule); err != nil {
return fmt.Errorf("adding egress rule %s: %v", k, err)
}
}
return nil
}
// moveLocalRule puts the local table's lookup after HolderRulePriority (it is at 0 by default), so the holder can
// route replies to its own address onwards to their node instead of delivering them to itself.
func moveLocalRule() error {
rules, err := netlink.RuleList(unix.AF_INET)
if err != nil {
return err
}
var at0, atLocal bool
for _, r := range rules {
if r.Table == unix.RT_TABLE_LOCAL && r.Src == nil && r.Dst == nil && r.Mark <= 0 {
switch {
case r.Priority <= 0:
// the kernel leaves out the priority attribute when it is 0: listed as unset (-1)
at0 = true
case r.Priority == LocalRulePriority:
atLocal = true
}
}
}
if !atLocal {
rule := netlink.NewRule()
rule.Priority = LocalRulePriority
rule.Table = unix.RT_TABLE_LOCAL
if err := netlink.RuleAdd(rule); err != nil {
return fmt.Errorf("adding local table rule at %d: %v", LocalRulePriority, err)
}
}
if at0 {
rule := netlink.NewRule()
rule.Priority = 0
rule.Table = unix.RT_TABLE_LOCAL
if err := netlink.RuleDel(rule); err != nil {
return fmt.Errorf("removing local table rule at 0: %v", err)
}
}
return nil
}
// ensureTunnels makes one ipip tunnel per peer (this node's bridge address to the peer's), and removes every
// other egress tunnel (including the shared one of earlier builds).
func ensureTunnels(peers []Node, local Local) error {
links, err := netlink.LinkList()
if err != nil {
return err
}
want := map[string]Node{}
for _, p := range peers {
want[TunnelName(p.Name)] = p
}
have := map[string]netlink.Link{}
for _, l := range links {
name := l.Attrs().Name
if name == legacyTunnel || strings.HasPrefix(name, TunnelPrefix) {
if _, ok := want[name]; ok {
have[name] = l
continue
}
if err := netlink.LinkDel(l); err != nil {
return fmt.Errorf("removing egress tunnel %s: %v", name, err)
}
}
}
if len(want) == 0 {
return nil
}
if local.WeaveIP == nil {
return fmt.Errorf("egress tunnels need the weave bridge's address")
}
br, err := netlink.LinkByName(bridge)
if err != nil {
return fmt.Errorf("weave bridge: %v", err)
}
mtu := br.Attrs().MTU - ipipOverhead
names := make([]string, 0, len(want))
for n := range want {
names = append(names, n)
}
sort.Strings(names)
for _, name := range names {
peer := want[name]
link := have[name]
if link != nil {
t, ok := link.(*netlink.Iptun)
if !ok || !t.Local.Equal(local.WeaveIP) || !t.Remote.Equal(peer.WeaveIP) || t.Attrs().MTU != mtu {
if err := netlink.LinkDel(link); err != nil {
return err
}
link = nil
}
}
if link == nil {
t := &netlink.Iptun{LinkAttrs: netlink.LinkAttrs{Name: name, MTU: mtu}, Local: local.WeaveIP, Remote: peer.WeaveIP, PMtuDisc: 1}
if err := netlink.LinkAdd(t); err != nil {
return fmt.Errorf("creating egress tunnel %s to %s: %v", name, peer.Name, err)
}
if link, err = netlink.LinkByName(name); err != nil {
return err
}
}
if err := tunnelSetup(link, local); err != nil {
return fmt.Errorf("%s: %v", name, err)
}
}
return nil
}
// tunnelSetup: the holder receives members' packets from its own address (accept_local); reverse-path filtering
// rejects what arrives on a device with no IPv4 address unless the reverse route leaves by that very device, so
// the tunnel carries this node's bridge address (never on the wire from here: the tunnel sets the outer source,
// and what crosses it is forwarded).
func tunnelSetup(link netlink.Link, local Local) error {
name := link.Attrs().Name
for k, v := range map[string]string{"accept_local": "1", "rp_filter": "2"} {
if err := os.WriteFile("/proc/sys/net/ipv4/conf/"+name+"/"+k, []byte(v), 0o644); err != nil {
return fmt.Errorf("sysctl %s: %v", k, err)
}
}
addrs, err := netlink.AddrList(link, unix.AF_INET)
if err != nil {
return err
}
has := false
for _, a := range addrs {
if a.IP.Equal(local.WeaveIP) {
has = true
continue
}
a := a
if err := netlink.AddrDel(link, &a); err != nil {
return fmt.Errorf("removing stale address %s: %v", a.IPNet, err)
}
}
if !has {
addr := &netlink.Addr{IPNet: &net.IPNet{IP: local.WeaveIP, Mask: net.CIDRMask(32, 32)}, Scope: unix.RT_SCOPE_LINK}
if err := netlink.AddrAdd(link, addr); err != nil {
return fmt.Errorf("address: %v", err)
}
}
return netlink.LinkSetUp(link)
}
+355
View File
@@ -0,0 +1,355 @@
package lb
import (
"fmt"
"net"
"strings"
"testing"
"time"
coreapi "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// the S1 fixtures: cp, w1 (holds 86.107.168.15), w2
func egressFor(name string, status EgressServiceStatus) *EgressService {
return &EgressService{ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: name},
Spec: EgressServiceSpec{Emit: EmitHolder}, Status: status}
}
func mailSvc() *coreapi.Service {
return svc("mail", "86.107.168.15", func(s *coreapi.Service) {
s.Spec.Selector = map[string]string{"app": "mail"}
})
}
func pod(name, node, ip string, labels map[string]string, mutate ...func(*coreapi.Pod)) *coreapi.Pod {
p := &coreapi.Pod{ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: name, Labels: labels,
CreationTimestamp: metav1.NewTime(time.Unix(1000, 0))},
Spec: coreapi.PodSpec{NodeName: node}, Status: coreapi.PodStatus{PodIP: ip, Phase: coreapi.PodRunning}}
for _, m := range mutate {
m(p)
}
return p
}
var mailLabels = map[string]string{"app": "mail"}
// the status the holder (w1) writes for three nodes, blocks in name order
func written() EgressServiceStatus {
return EgressServiceStatus{Host: "w1", Address: "86.107.168.15", Blocks: []PortBlock{
{Node: "cp", From: 61000, To: 61511}, {Node: "w1", From: 61512, To: 62023}, {Node: "w2", From: 62024, To: 62535}}}
}
func TestBlocksAreStableAndCapped(t *testing.T) {
got := AllocateBlocks(nil, []string{"w2", "cp", "w1"})
if !sameStatus(EgressServiceStatus{Blocks: got}, EgressServiceStatus{Blocks: written().Blocks}) {
t.Fatalf("fresh blocks: %+v", got)
}
// a node leaves, another joins: survivors keep their blocks, the newcomer takes the freed lowest one
got = AllocateBlocks(got, []string{"w1", "w2", "w3"})
want := []PortBlock{{"w1", 61512, 62023}, {"w2", 62024, 62535}, {"w3", 61000, 61511}}
if !sameStatus(EgressServiceStatus{Blocks: got}, EgressServiceStatus{Blocks: want}) {
t.Fatalf("after churn: %+v", got)
}
// misaligned or duplicate blocks are not kept
got = AllocateBlocks([]PortBlock{{"w1", 61001, 61512}, {"w2", 61000, 61511}, {"w3", 61000, 61511}}, []string{"w1", "w2", "w3"})
for _, b := range got {
if (b.From-BlockBase)%BlockSize != 0 || b.To != b.From+BlockSize-1 {
t.Fatalf("invalid block kept: %+v", got)
}
}
if got[1].Node != "w2" || got[1].From != 61000 {
t.Fatalf("first valid claim wins: %+v", got)
}
var many []string
for i := 0; i < BlockCount+2; i++ {
many = append(many, string(rune('a'+i)))
}
if n := len(AllocateBlocks(nil, many)); n != BlockCount {
t.Fatalf("more blocks than exist: %d", n)
}
for _, b := range AllocateBlocks(nil, many) {
if b.To > 65535 || b.From < 61000 {
t.Fatalf("block outside the range: %+v", b)
}
}
}
func TestHolderWritesStatusAndMembersWaitForIt(t *testing.T) {
e := egressFor("mail", EgressServiceStatus{})
pods := []*coreapi.Pod{pod("mail-0", "w2", "10.44.0.9", mailLabels)}
d := ComputeEgress("w1", nodes, []*coreapi.Service{mailSvc()}, []*EgressService{e}, nil, nil)
if len(d.Status) != 1 || !sameStatus(d.Status[0].Status, written()) {
t.Fatalf("holder's status: %+v", d.Status)
}
// until the status is in the API, a member's outside traffic goes nowhere (fail closed)
d = ComputeEgress("w2", nodes, []*coreapi.Service{mailSvc()}, []*EgressService{e}, pods, nil)
if len(d.Status) != 0 || len(d.Members) != 1 || !d.Members[0].Unreachable {
t.Fatalf("member before status: %+v", d)
}
if d.Tunnelled("w2") {
t.Fatalf("no tunnel for an unreachable member")
}
}
func TestMemberTunnelsToHolderWhichRoutesItsBlockBack(t *testing.T) {
e := egressFor("mail", written())
svcs := []*coreapi.Service{mailSvc()}
pods := []*coreapi.Pod{
pod("mail-0", "w2", "10.44.0.9", mailLabels),
pod("web", "w2", "10.44.0.10", map[string]string{"app": "web"}), // not a member (A4)
}
d := ComputeEgress("w2", nodes, svcs, []*EgressService{e}, pods, nil)
if len(d.Members) != 1 || d.Members[0].Pod != "gitea/mail-0" {
t.Fatalf("members: %+v", d.Members)
}
m := d.Members[0]
if m.Unreachable || !m.Address.Equal(net.ParseIP("86.107.168.15")) || m.From != 62024 || m.To != 62535 || m.Holder.Name != "w1" {
t.Fatalf("member path: %+v", m)
}
if !d.Tunnelled("w2") || len(d.Peers("w2")) != 1 || d.Peers("w2")[0].Name != "w1" {
t.Fatalf("w2 tunnels to w1 only: %+v", d.Peers("w2"))
}
if len(d.Status) != 0 || len(d.Routes) != 0 {
t.Fatalf("only the holder writes and routes: %+v", d)
}
// the holder: blocks of the other nodes route back to them; its own block is plain conntrack
h := ComputeEgress("w1", nodes, svcs, []*EgressService{e}, nil, nil)
if len(h.Status) != 0 {
t.Fatalf("status already right: %+v", h.Status)
}
if len(h.Routes) != 2 || h.Routes[0].Peer.Name != "cp" || h.Routes[1].Peer.Name != "w2" || h.Routes[1].From != 62024 {
t.Fatalf("holder routes: %+v", h.Routes)
}
// a member on the holder leaves from there
hm := ComputeEgress("w1", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{pod("mail-1", "w1", "10.40.0.4", mailLabels)}, nil)
if len(hm.Members) != 1 || hm.Members[0].Holder.Name != "w1" || hm.Members[0].From != 61512 {
t.Fatalf("member on the holder: %+v", hm.Members)
}
}
func TestEgressFailsClosed(t *testing.T) {
pods := []*coreapi.Pod{pod("mail-0", "w2", "10.44.0.9", mailLabels)}
// no node holds the address: status cleared (by the first node by name), member dropped
unheld := svc("mail", "86.107.168.99", func(s *coreapi.Service) { s.Spec.Selector = mailLabels })
for _, self := range []string{"cp", "w2"} {
d := ComputeEgress(self, nodes, []*coreapi.Service{unheld}, []*EgressService{egressFor("mail", written())}, pods, nil)
clears := len(d.Status) == 1 && d.Status[0].Status.Host == ""
if clears != (self == "cp") {
t.Fatalf("%s clears=%v: %+v", self, clears, d.Status)
}
if self == "w2" && (len(d.Members) != 1 || !d.Members[0].Unreachable) {
t.Fatalf("member of an unheld address: %+v", d.Members)
}
}
// two EgressServices select the pod: dropped, reported
other := svc("mail2", "86.107.168.15", func(s *coreapi.Service) { s.Spec.Selector = mailLabels })
d := ComputeEgress("w2", nodes, []*coreapi.Service{mailSvc(), other},
[]*EgressService{egressFor("mail", written()), egressFor("mail2", written())}, pods, nil)
if len(d.Members) != 1 || !d.Members[0].Unreachable || !strings.Contains(strings.Join(d.Problems, "\n"), "several EgressServices") {
t.Fatalf("double member: %+v", d)
}
// a spec this agent does not implement is refused, not guessed at
bad := egressFor("mail", written())
bad.Spec.Emit = "Direct"
d = ComputeEgress("w2", nodes, []*coreapi.Service{mailSvc()}, []*EgressService{bad}, pods, nil)
if len(d.Members) != 1 || !d.Members[0].Unreachable {
t.Fatalf("unknown emit mode: %+v", d)
}
// hostNetwork pods are never members (their traffic is the node's)
hn := pod("mail-h", "w2", "86.107.168.88", mailLabels, func(p *coreapi.Pod) { p.Spec.HostNetwork = true })
d = ComputeEgress("w2", nodes, []*coreapi.Service{mailSvc()}, []*EgressService{egressFor("mail", written())}, []*coreapi.Pod{hn}, nil)
if len(d.Members) != 0 {
t.Fatalf("hostNetwork pod became a member: %+v", d.Members)
}
}
func TestAdmissionDecidesWhoOwnsAnIP(t *testing.T) {
e := egressFor("mail", written())
svcs := []*coreapi.Service{mailSvc()}
old := pod("mail-0", "w2", "10.44.0.9", mailLabels, func(p *coreapi.Pod) {
now := metav1.Now()
p.DeletionTimestamp = &now
})
newer := pod("web", "w2", "", map[string]string{"app": "web"}) // API has no IP for it yet
// the CNI plugin just gave 10.44.0.9 to web: the terminating member must not keep it (A4)
d := ComputeEgress("w2", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{old, newer},
map[string]Admission{"10.44.0.9": {Namespace: "gitea", Name: "web"}})
if len(d.Members) != 0 {
t.Fatalf("reused IP kept the old member's rules: %+v", d.Members)
}
// and the other way round: a member admitted before the API shows its IP is protected at once (A1)
fresh := pod("mail-1", "w2", "", mailLabels)
d = ComputeEgress("w2", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{fresh},
map[string]Admission{"10.44.0.11": {Namespace: "gitea", Name: "mail-1"}})
if len(d.Members) != 1 || d.Members[0].IP.String() != "10.44.0.11" || d.Members[0].Unreachable {
t.Fatalf("admitted member: %+v", d.Members)
}
// without an admission, a live pod wins a shared IP over a terminating one
live := pod("web", "w2", "10.44.0.9", map[string]string{"app": "web"})
d = ComputeEgress("w2", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{old, live}, nil)
if len(d.Members) != 0 {
t.Fatalf("terminating member won the IP: %+v", d.Members)
}
}
func TestRenderedEgressRules(t *testing.T) {
_, podNet, _ := net.ParseCIDR("10.32.0.0/12")
e := egressFor("mail", written())
svcs := []*coreapi.Service{mailSvc()}
toW1, toW2, toCP := TunnelName("w1"), TunnelName("w2"), TunnelName("cp")
member := ComputeEgress("w2", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{pod("mail-0", "w2", "10.44.0.9", mailLabels)}, nil)
r := Render(Desired{}, member, "w2", podNet, nil)
for _, want := range []string{
"-A REWEAVE-LB-CLASS -d 10.32.0.0/12 -j RETURN\n",
"-A REWEAVE-LB-CLASS -d 86.107.168.24/32 -j RETURN\n", // a node is not outside
"-A REWEAVE-LB-CLASS -s 10.44.0.9/32 -m comment --comment \"gitea/mail-0\" -j CONNMARK --set-xmark 0x100000/0x100000\n",
"-A REWEAVE-LB-CLASS -s 10.44.0.9/32 -j MARK --set-xmark 0x100000/0x100000\n",
// port protocols within this node's block, into the holder's own tunnel
"-A REWEAVE-LB-POST -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -p tcp -j SNAT --to-source 86.107.168.15:62024-62535\n",
"-A REWEAVE-LB-POST -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -p udp -j SNAT --to-source 86.107.168.15:62024-62535\n",
"-A REWEAVE-LB-POST -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -p sctp -j SNAT --to-source 86.107.168.15:62024-62535\n",
// ping within the block too (the echo identifier is NAT's port for ICMP)
"-A REWEAVE-LB-POST -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -p icmp -j SNAT --to-source 86.107.168.15:62024-62535\n",
// everything else (GRE, ...) from the address alone
"-A REWEAVE-LB-POST -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -j SNAT --to-source 86.107.168.15\n",
} {
if !strings.Contains(r.Nat, want) {
t.Fatalf("member nat lacks %q:\n%s", want, r.Nat)
}
}
// the catch-all SNAT comes after the port-ranged ones (or ports would not stay in the block)
if strings.Index(r.Nat, "-j SNAT --to-source 86.107.168.15\n") < strings.Index(r.Nat, "-p icmp -j SNAT") {
t.Fatalf("SNAT order:\n%s", r.Nat)
}
for _, want := range []string{
"-A REWEAVE-LB-FWD -s 10.44.0.9/32 -o " + toW1 + " -m mark --mark 0x100000/0x100000 -j RETURN\n",
"-A REWEAVE-LB-FWD -s 10.44.0.9/32 -m mark --mark 0x100000/0x100000 -m comment --comment \"gitea/mail-0\" -j DROP\n",
"-A REWEAVE-LB-FWD -i rweg+ -d 86.107.168.15/32 -j DROP\n",
"-A REWEAVE-LB-FWD -i rweg+ -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN\n",
"-A REWEAVE-LB-FWD -i rweg+ -j DROP\n",
"-A REWEAVE-LB-IN -p 4 -s 10.40.0.0/32 -m mac --mac-source aa:aa:aa:aa:aa:02 -j RETURN\n",
"-A REWEAVE-LB-IN -p 4 -j DROP\n",
} {
if !strings.Contains(r.Filter, want) {
t.Fatalf("member filter lacks %q:\n%s", want, r.Filter)
}
}
for _, want := range []string{
"-A REWEAVE-LB-MARK -m conntrack --ctdir ORIGINAL -m connmark --mark 0x100000/0x100000 -j MARK --set-xmark 0x100000/0x100000\n",
"-A REWEAVE-LB-MSS -o rweg+ -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu\n",
} {
if !strings.Contains(r.Mangle, want) {
t.Fatalf("member mangle lacks %q:\n%s", want, r.Mangle)
}
}
if strings.Contains(r.Mangle, "--set-xmark 0x") && strings.Contains(r.Mangle, "-i "+toW1) {
t.Fatalf("a member's node does not tag what its holder sends back:\n%s", r.Mangle)
}
// the guard's DROP comes after the member's own RETURN: nothing else of its leaves
if strings.Index(r.Filter, toW1+" -m mark --mark 0x100000/0x100000 -j RETURN") > strings.Index(r.Filter, "\"gitea/mail-0\" -j DROP") {
t.Fatalf("guard order:\n%s", r.Filter)
}
// the holder: tags each tunnel's connections with its node, accepts each node's block, nothing foreign
h := ComputeEgress("w1", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{pod("mail-1", "w1", "10.40.0.4", mailLabels)}, nil)
r = Render(Desired{}, h, "w1", podNet, nil)
idx := peerIndex(h, "w1")
tagW2 := fmt.Sprintf("0x%x", peerTag(idx["w2"]))
for _, want := range []string{
"-A REWEAVE-LB-MARK -m conntrack --ctdir REPLY -m connmark --mark 0x200000/0x200000 -j CONNMARK --restore-mark --nfmask 0xff200000 --ctmask 0xff200000\n",
"-A REWEAVE-LB-MARK -i " + toW2 + " -m conntrack --ctstate NEW -m connmark --mark 0x0/0xff200000 -m comment --comment \"from w2\" -j CONNMARK --set-xmark " + tagW2 + "/0xff200000\n",
} {
if !strings.Contains(r.Mangle, want) {
t.Fatalf("holder mangle lacks %q:\n%s", want, r.Mangle)
}
}
for _, want := range []string{
"-A REWEAVE-LB-FWD -i " + toW2 + " -m conntrack --ctdir ORIGINAL -m connmark ! --mark " + tagW2 + "/0xff200000 -j DROP\n",
"-A REWEAVE-LB-FWD -i " + toW2 + " -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -p tcp -m tcp --sport 62024:62535 -m comment --comment \"block of w2\" -j RETURN\n",
"-A REWEAVE-LB-FWD -i " + toCP + " -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -p sctp -m sctp --sport 61000:61511 -m comment --comment \"block of cp\" -j RETURN\n",
"-A REWEAVE-LB-FWD -i " + toW2 + " -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -p udp -j DROP\n",
"-A REWEAVE-LB-FWD -i " + toW2 + " -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -j RETURN\n",
"-A REWEAVE-LB-FWD -s 10.40.0.4/32 -o weave -m mark --mark 0x100000/0x100000 -j DROP\n",
} {
if !strings.Contains(r.Filter, want) {
t.Fatalf("holder filter lacks %q:\n%s", want, r.Filter)
}
}
// foreign tags are refused before anything of that tunnel is accepted; a port outside the block before the
// catch-all accept
if strings.Index(r.Filter, "! --mark "+tagW2) > strings.Index(r.Filter, "\"block of w2\"") ||
strings.Index(r.Filter, "-i "+toW2+" -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -p udp -j DROP") >
strings.Index(r.Filter, "-i "+toW2+" -m conntrack --ctdir ORIGINAL -s 86.107.168.15/32 -j RETURN") {
t.Fatalf("holder filter order:\n%s", r.Filter)
}
if !strings.Contains(r.Nat, "-A REWEAVE-LB-POST -s 10.40.0.4/32 ! -o weave -m mark --mark 0x100000/0x100000 -p tcp -j SNAT --to-source 86.107.168.15:61512-62023\n") {
t.Fatalf("holder's own member:\n%s", r.Nat)
}
for _, line := range strings.Split(r.Nat+r.Filter, "\n") {
if strings.Contains(line, "10.44.0.10") {
t.Fatalf("a rule names a non-member: %s", line)
}
}
}
func TestTunnelNames(t *testing.T) {
a, b := TunnelName("reweave-worker-fe19b040"), TunnelName("reweave-cp-94a8db43")
if a == b || len(a) > 15 || !strings.HasPrefix(a, TunnelPrefix) || a != TunnelName("reweave-worker-fe19b040") {
t.Fatalf("tunnel names must be distinct, stable, short: %q %q", a, b)
}
}
func TestPolicyRules(t *testing.T) {
e := egressFor("mail", written())
svcs := []*coreapi.Service{mailSvc()}
h := ComputeEgress("w1", nodes, svcs, []*EgressService{e}, nil, nil)
rules := wantRules(h, "w1")
tables := peerTables(h, "w1")
idx := peerIndex(h, "w1")
ports, tags := 0, 0
for _, r := range rules {
if r.priority != HolderRulePriority || r.src != "" {
t.Fatalf("holder rule: %+v", r)
}
switch {
case r.ipproto != 0:
ports++
if r.dst != "86.107.168.15/32" || r.mark != 0 {
t.Fatalf("port rule: %+v", r)
}
if (r.from == 62024) != (r.table == tables["w2"]) || (r.from == 61000) != (r.table == tables["cp"]) {
t.Fatalf("block routed to the wrong node: %+v %v", r, tables)
}
default:
tags++
if r.mask != peerMask || r.dst != "" {
t.Fatalf("tag rule: %+v", r)
}
for name, i := range idx {
if r.mark == peerTag(i) && r.table != tables[name] {
t.Fatalf("tag of %s routed to table %d, want %d", name, r.table, tables[name])
}
}
}
}
if ports != 2*len(portProtocols) || tags != 2 {
t.Fatalf("2 peers: %d port rules (tcp, udp, sctp each), %d tag rules: %+v", ports, tags, rules)
}
if HolderRulePriority >= LocalRulePriority {
t.Fatalf("holder rules must precede the local table")
}
m := ComputeEgress("w2", nodes, svcs, []*EgressService{e}, []*coreapi.Pod{pod("mail-0", "w2", "10.44.0.9", mailLabels)}, nil)
rules = wantRules(m, "w2")
if len(rules) != 1 || rules[0].src != "10.44.0.9/32" || rules[0].mark != EgressMark || rules[0].mask != EgressMark ||
rules[0].table != peerTables(m, "w2")["w1"] {
t.Fatalf("member rule: %+v", rules)
}
u := ComputeEgress("w2", nodes, svcs, []*EgressService{egressFor("mail", EgressServiceStatus{})}, []*coreapi.Pod{pod("mail-0", "w2", "10.44.0.9", mailLabels)}, nil)
rules = wantRules(u, "w2")
if len(rules) != 1 || rules[0].table != EgressTableBase {
t.Fatalf("unreachable member must hit the unreachable table: %+v", rules)
}
}
+308
View File
@@ -0,0 +1,308 @@
// Package lb gives LoadBalancer Services an address that a node holds, and
// forwards that address's traffic to the Service's endpoints with the
// client's address intact.
//
// A Service opts in with spec.loadBalancerClass = Class and names its
// address in spec.loadBalancerIP. The node that holds the address (has it on
// one of its interfaces, as published in its AnnAddresses annotation) is the
// address's holder: it alone writes the Service's status (ipMode Proxy, so
// kube-proxy installs no rules for the address) and DNATs the address's
// ports to ready endpoints, without SNAT. Every other node marks the
// connections that the holder forwards to its pods, and routes their replies
// back to the holder over weave, so that the holder's conntrack can undo the
// DNAT and the reply leaves from the address the client connected to.
//
// Compute is the whole policy as a pure function of what the API shows; the
// datapath and the controller only carry it out.
package lb
import (
"fmt"
"hash/fnv"
"net"
"sort"
"strings"
coreapi "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
)
const (
// Class is the spec.loadBalancerClass a Service sets to be served by this agent.
Class = "reweave.levk.dev/node-address"
// AnnAddresses lists the IPv4 addresses a node holds on its host interfaces (comma-separated).
AnnAddresses = "reweave.levk.dev/addresses"
// AnnWeave is "<ip>,<mac>" of the node's weave bridge: where replies to its forwards go.
AnnWeave = "reweave.levk.dev/weave"
serviceNameLabel = "kubernetes.io/service-name"
)
// Node is what one node publishes about itself.
type Node struct {
Name string
Addresses []net.IP
WeaveIP net.IP
WeaveMAC net.HardwareAddr
}
// Holds reports whether the node has ip on one of its host interfaces.
func (n Node) Holds(ip net.IP) bool {
for _, a := range n.Addresses {
if a.Equal(ip) {
return true
}
}
return false
}
// ParseNode reads a Node's annotations. A node that has published nothing
// (yet) holds nothing and cannot take replies.
func ParseNode(n *coreapi.Node) (Node, error) {
out := Node{Name: n.Name}
if s := n.Annotations[AnnAddresses]; s != "" {
for _, f := range strings.Split(s, ",") {
ip := net.ParseIP(strings.TrimSpace(f)).To4()
if ip == nil {
return out, fmt.Errorf("node %s: %s: %q is not an IPv4 address", n.Name, AnnAddresses, f)
}
out.Addresses = append(out.Addresses, ip)
}
}
if s := n.Annotations[AnnWeave]; s != "" {
f := strings.Split(s, ",")
if len(f) != 2 {
return out, fmt.Errorf("node %s: %s: want \"<ip>,<mac>\", got %q", n.Name, AnnWeave, s)
}
ip := net.ParseIP(f[0]).To4()
mac, err := net.ParseMAC(f[1])
if ip == nil || err != nil {
return out, fmt.Errorf("node %s: %s: want \"<ip>,<mac>\", got %q", n.Name, AnnWeave, s)
}
out.WeaveIP, out.WeaveMAC = ip, mac
}
return out, nil
}
// FormatAddresses is the AnnAddresses value for a set of addresses (sorted, so it only changes when the set does).
func FormatAddresses(ips []net.IP) string {
s := make([]string, 0, len(ips))
for _, ip := range ips {
s = append(s, ip.String())
}
sort.Strings(s)
return strings.Join(s, ",")
}
// Endpoint is one ready backend of one Service port.
type Endpoint struct {
IP net.IP
Port int32
}
func (e Endpoint) String() string { return fmt.Sprintf("%s:%d", e.IP, e.Port) }
// Port is one Service port at the held address.
type Port struct {
Protocol coreapi.Protocol
Port int32
Endpoints []Endpoint
}
// Forward is a Service whose address this node holds.
type Forward struct {
Namespace, Name string
Address net.IP
Ports []Port
}
// Status is a write to a Service's status.loadBalancer: Address set, or cleared when nil.
type Status struct {
Namespace, Name string
Address net.IP
}
// Holder is another node that forwards held addresses' connections to pods; Mark identifies its connections here.
type Holder struct {
Node Node
Mark uint32 // 1..MaxMarks, in the connmark's top byte
}
// MaxMarks is how many holders a node can tell apart (the top byte of the mark, 0 meaning none).
const MaxMarks = 250
// Desired is what one node must do.
type Desired struct {
Forwards []Forward
Holders []Holder
Status []Status
Problems []string // what an operator should hear about (logged and evented by the controller)
}
// eligible reports whether the Service asks for this agent.
func eligible(s *coreapi.Service) bool {
return s.Spec.Type == coreapi.ServiceTypeLoadBalancer && s.Spec.LoadBalancerClass != nil &&
*s.Spec.LoadBalancerClass == Class
}
// shown is the address status.loadBalancer currently shows (nil if none or not a single IP).
func shown(s *coreapi.Service) net.IP {
if len(s.Status.LoadBalancer.Ingress) != 1 {
return nil
}
return net.ParseIP(s.Status.LoadBalancer.Ingress[0].IP).To4()
}
func hasStatus(s *coreapi.Service) bool { return len(s.Status.LoadBalancer.Ingress) != 0 }
// Compute decides what node `self` does, given every node, every Service and every EndpointSlice.
func Compute(self string, nodes []Node, services []*coreapi.Service, slices []*discovery.EndpointSlice) Desired {
var d Desired
sortedNodes := append([]Node(nil), nodes...)
sort.Slice(sortedNodes, func(i, j int) bool { return sortedNodes[i].Name < sortedNodes[j].Name })
holdersOf := func(ip net.IP) []Node {
var hs []Node
for _, n := range sortedNodes {
if ip != nil && n.Holds(ip) {
hs = append(hs, n)
}
}
return hs
}
// who may write a status that shows `ip`: its single holder, or (nobody holds it) the first node by name
writerFor := func(ip net.IP) string {
if hs := holdersOf(ip); len(hs) == 1 {
return hs[0].Name
}
if len(sortedNodes) > 0 {
return sortedNodes[0].Name
}
return ""
}
holding := map[string]bool{} // nodes that forward some address
svcs := append([]*coreapi.Service(nil), services...)
sort.Slice(svcs, func(i, j int) bool { return key(svcs[i]) < key(svcs[j]) })
for _, s := range svcs {
if !eligible(s) {
// loadBalancerClass is immutable and only allowed on type LoadBalancer, so a
// Service never leaves the class while keeping a status we wrote
continue
}
want := net.ParseIP(s.Spec.LoadBalancerIP).To4()
var holder *Node
switch hs := holdersOf(want); {
case want == nil:
d.Problems = append(d.Problems, fmt.Sprintf("%s: spec.loadBalancerIP must name an IPv4 address a node holds (got %q)", key(s), s.Spec.LoadBalancerIP))
case len(hs) == 0:
d.Problems = append(d.Problems, fmt.Sprintf("%s: no node holds %s", key(s), want))
case len(hs) > 1:
names := make([]string, len(hs))
for i, h := range hs {
names[i] = h.Name
}
d.Problems = append(d.Problems, fmt.Sprintf("%s: %s is held by several nodes (%s); serving it from none", key(s), want, strings.Join(names, ", ")))
default:
holder = &hs[0]
}
if holder == nil {
// fail closed: nobody forwards, and the status must not claim an address
if hasStatus(s) && writerFor(shown(s)) == self {
d.Status = append(d.Status, Status{Namespace: s.Namespace, Name: s.Name})
}
continue
}
holding[holder.Name] = true
if holder.Name != self {
continue
}
if cur := shown(s); cur == nil || !cur.Equal(want) {
d.Status = append(d.Status, Status{Namespace: s.Namespace, Name: s.Name, Address: want})
}
d.Forwards = append(d.Forwards, Forward{Namespace: s.Namespace, Name: s.Name, Address: want, Ports: ports(s, slices)})
}
var others []Node
for _, n := range sortedNodes {
if holding[n.Name] && n.Name != self {
if n.WeaveIP == nil {
d.Problems = append(d.Problems, fmt.Sprintf("node %s holds an address but has not published its weave bridge; replies to its forwards cannot reach it", n.Name))
continue
}
others = append(others, n)
}
}
d.Holders = assignMarks(others)
return d
}
// assignMarks gives each holder a mark from its name, probing past collisions in name order: stable while the set is.
func assignMarks(nodes []Node) []Holder {
taken := map[uint32]bool{}
out := make([]Holder, 0, len(nodes))
for _, n := range nodes {
if len(out) == MaxMarks {
break
}
h := fnv.New32a()
h.Write([]byte(n.Name))
m := h.Sum32()%MaxMarks + 1
for taken[m] {
m = m%MaxMarks + 1
}
taken[m] = true
out = append(out, Holder{Node: n, Mark: m})
}
return out
}
func key(s *coreapi.Service) string { return s.Namespace + "/" + s.Name }
// ports pairs each Service port with its ready endpoints.
func ports(s *coreapi.Service, slices []*discovery.EndpointSlice) []Port {
var out []Port
for _, sp := range s.Spec.Ports {
proto := sp.Protocol
if proto == "" {
proto = coreapi.ProtocolTCP
}
seen := map[string]bool{}
var eps []Endpoint
for _, sl := range slices {
if sl.Namespace != s.Namespace || sl.Labels[serviceNameLabel] != s.Name || sl.AddressType != discovery.AddressTypeIPv4 {
continue
}
for _, p := range sl.Ports {
pname, pproto := "", coreapi.ProtocolTCP
if p.Name != nil {
pname = *p.Name
}
if p.Protocol != nil {
pproto = *p.Protocol
}
if pname != sp.Name || pproto != proto || p.Port == nil {
continue
}
for _, ep := range sl.Endpoints {
if ep.Conditions.Ready != nil && !*ep.Conditions.Ready {
continue
}
for _, a := range ep.Addresses {
ip := net.ParseIP(a).To4()
if ip == nil {
continue
}
e := Endpoint{IP: ip, Port: *p.Port}
if !seen[e.String()] {
seen[e.String()] = true
eps = append(eps, e)
}
}
}
}
}
sort.Slice(eps, func(i, j int) bool { return eps[i].String() < eps[j].String() })
out = append(out, Port{Protocol: proto, Port: sp.Port, Endpoints: eps})
}
return out
}
+253
View File
@@ -0,0 +1,253 @@
package lb
import (
"fmt"
"net"
"strings"
"testing"
coreapi "k8s.io/api/core/v1"
discovery "k8s.io/api/discovery/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func mac(s string) net.HardwareAddr {
m, err := net.ParseMAC(s)
if err != nil {
panic(err)
}
return m
}
var (
cp = Node{Name: "cp", Addresses: []net.IP{net.ParseIP("86.107.168.24").To4()},
WeaveIP: net.ParseIP("10.32.0.1").To4(), WeaveMAC: mac("aa:aa:aa:aa:aa:01")}
w1 = Node{Name: "w1", Addresses: []net.IP{net.ParseIP("86.107.168.15").To4()},
WeaveIP: net.ParseIP("10.40.0.0").To4(), WeaveMAC: mac("aa:aa:aa:aa:aa:02")}
w2 = Node{Name: "w2", Addresses: []net.IP{net.ParseIP("86.107.168.88").To4()},
WeaveIP: net.ParseIP("10.44.0.0").To4(), WeaveMAC: mac("aa:aa:aa:aa:aa:03")}
nodes = []Node{w2, cp, w1}
)
func svc(name, ip string, mutate ...func(*coreapi.Service)) *coreapi.Service {
class := Class
s := &coreapi.Service{
ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: name},
Spec: coreapi.ServiceSpec{
Type: coreapi.ServiceTypeLoadBalancer,
LoadBalancerClass: &class,
LoadBalancerIP: ip,
Ports: []coreapi.ServicePort{
{Name: "ssh", Protocol: coreapi.ProtocolTCP, Port: 22},
{Name: "https", Port: 443}, // protocol defaults to TCP
},
},
}
for _, m := range mutate {
m(s)
}
return s
}
func shows(ip string) func(*coreapi.Service) {
return func(s *coreapi.Service) {
s.Status.LoadBalancer.Ingress = []coreapi.LoadBalancerIngress{{IP: ip}}
}
}
func ptr[T any](v T) *T { return &v }
func slice(svcName string, eps map[string]bool, ports map[string]int32) *discovery.EndpointSlice {
sl := &discovery.EndpointSlice{
ObjectMeta: metav1.ObjectMeta{Namespace: "gitea", Name: svcName + "-x", Labels: map[string]string{serviceNameLabel: svcName}},
AddressType: discovery.AddressTypeIPv4,
}
for ip, ready := range eps {
sl.Endpoints = append(sl.Endpoints, discovery.Endpoint{Addresses: []string{ip}, Conditions: discovery.EndpointConditions{Ready: ptr(ready)}})
}
for name, port := range ports {
sl.Ports = append(sl.Ports, discovery.EndpointPort{Name: ptr(name), Protocol: ptr(coreapi.ProtocolTCP), Port: ptr(port)})
}
return sl
}
func TestHolderForwardsToReadyEndpointsAndWritesStatus(t *testing.T) {
s := svc("gitea", "86.107.168.15")
sl := slice("gitea", map[string]bool{"10.40.0.7": true, "10.44.0.3": true, "10.44.0.9": false},
map[string]int32{"ssh": 2222, "https": 3000})
d := Compute("w1", nodes, []*coreapi.Service{s}, []*discovery.EndpointSlice{sl})
if len(d.Problems) != 0 {
t.Fatalf("problems: %v", d.Problems)
}
if len(d.Forwards) != 1 || !d.Forwards[0].Address.Equal(net.ParseIP("86.107.168.15")) {
t.Fatalf("forwards: %+v", d.Forwards)
}
f := d.Forwards[0]
if len(f.Ports) != 2 || f.Ports[0].Port != 22 || f.Ports[1].Port != 443 || f.Ports[1].Protocol != coreapi.ProtocolTCP {
t.Fatalf("ports: %+v", f.Ports)
}
if got := f.Ports[0].Endpoints; len(got) != 2 || got[0].String() != "10.40.0.7:2222" || got[1].String() != "10.44.0.3:2222" {
t.Fatalf("ssh endpoints (not-ready one must be left out): %v", got)
}
if got := f.Ports[1].Endpoints; len(got) != 2 || got[0].Port != 3000 {
t.Fatalf("https endpoints: %v", got)
}
if len(d.Status) != 1 || !d.Status[0].Address.Equal(net.ParseIP("86.107.168.15")) {
t.Fatalf("status: %+v", d.Status)
}
if len(d.Holders) != 0 {
t.Fatalf("a holder does not steer its own replies: %+v", d.Holders)
}
// already shown: nothing to write
d = Compute("w1", nodes, []*coreapi.Service{svc("gitea", "86.107.168.15", shows("86.107.168.15"))}, nil)
if len(d.Status) != 0 {
t.Fatalf("status rewritten: %+v", d.Status)
}
}
func TestOtherNodesSteerRepliesToTheHolderAndNeverWrite(t *testing.T) {
s := svc("gitea", "86.107.168.15")
for _, self := range []string{"cp", "w2"} {
d := Compute(self, nodes, []*coreapi.Service{s}, nil)
if len(d.Forwards) != 0 || len(d.Status) != 0 {
t.Fatalf("%s: only the holder forwards and writes: %+v", self, d)
}
if len(d.Holders) != 1 || d.Holders[0].Node.Name != "w1" || d.Holders[0].Mark < 1 || d.Holders[0].Mark > MaxMarks {
t.Fatalf("%s: holders: %+v", self, d.Holders)
}
}
}
func TestUnheldOrContestedAddressFailsClosed(t *testing.T) {
// nobody holds it: no forward anywhere; the stale status is cleared by exactly one node (first by name)
s := svc("gitea", "86.107.168.99", shows("86.107.168.99"))
for _, self := range []string{"cp", "w1", "w2"} {
d := Compute(self, nodes, []*coreapi.Service{s}, nil)
if len(d.Forwards) != 0 || len(d.Holders) != 0 || len(d.Problems) != 1 || !strings.Contains(d.Problems[0], "no node holds") {
t.Fatalf("%s: %+v", self, d)
}
clears := len(d.Status) == 1 && d.Status[0].Address == nil
if clears != (self == "cp") {
t.Fatalf("%s: clears=%v", self, clears)
}
}
// two nodes claim it: neither serves it, its holder's view clears the status
both := w2
both.Addresses = append([]net.IP{net.ParseIP("86.107.168.15").To4()}, w2.Addresses...)
d := Compute("w1", []Node{cp, w1, both}, []*coreapi.Service{svc("gitea", "86.107.168.15", shows("86.107.168.15"))}, nil)
if len(d.Forwards) != 0 || len(d.Problems) != 1 || !strings.Contains(d.Problems[0], "several nodes") {
t.Fatalf("contested: %+v", d)
}
// a bad request is reported, not guessed at
d = Compute("w1", nodes, []*coreapi.Service{svc("gitea", "")}, nil)
if len(d.Forwards) != 0 || len(d.Problems) != 1 || !strings.Contains(d.Problems[0], "spec.loadBalancerIP") {
t.Fatalf("no address: %+v", d)
}
}
func TestOnlyOptedInLoadBalancersAreServed(t *testing.T) {
other := svc("a", "86.107.168.15", func(s *coreapi.Service) { s.Spec.LoadBalancerClass = ptr("example.com/other") })
noClass := svc("b", "86.107.168.15", func(s *coreapi.Service) { s.Spec.LoadBalancerClass = nil })
clusterIP := svc("c", "86.107.168.15", func(s *coreapi.Service) {
s.Spec.Type = coreapi.ServiceTypeClusterIP
})
d := Compute("w1", nodes, []*coreapi.Service{other, noClass, clusterIP}, nil)
if len(d.Forwards)+len(d.Status)+len(d.Holders)+len(d.Problems) != 0 {
t.Fatalf("touched a Service that did not opt in: %+v", d)
}
}
func TestParseNodeAndAnnotationsRoundTrip(t *testing.T) {
l := Local{Addresses: []net.IP{net.ParseIP("86.107.168.88").To4(), net.ParseIP("86.107.168.15").To4()},
WeaveIP: net.ParseIP("10.44.0.0").To4(), WeaveMAC: mac("aa:aa:aa:aa:aa:03")}
a := l.Annotations()
if a[AnnAddresses] != "86.107.168.15,86.107.168.88" || a[AnnWeave] != "10.44.0.0,aa:aa:aa:aa:aa:03" {
t.Fatalf("annotations: %v", a)
}
n, err := ParseNode(&coreapi.Node{ObjectMeta: metav1.ObjectMeta{Name: "w2", Annotations: a}})
if err != nil || !n.Holds(net.ParseIP("86.107.168.15")) || !n.WeaveIP.Equal(l.WeaveIP) || n.WeaveMAC.String() != "aa:aa:aa:aa:aa:03" {
t.Fatalf("parsed %+v, %v", n, err)
}
for _, bad := range []map[string]string{{AnnAddresses: "1.2.3.4,nope"}, {AnnWeave: "10.44.0.0"}, {AnnWeave: "x,aa:aa:aa:aa:aa:03"}} {
if _, err := ParseNode(&coreapi.Node{ObjectMeta: metav1.ObjectMeta{Name: "w2", Annotations: bad}}); err == nil {
t.Fatalf("accepted %v", bad)
}
}
if n, err := ParseNode(&coreapi.Node{ObjectMeta: metav1.ObjectMeta{Name: "new"}}); err != nil || len(n.Addresses) != 0 || n.WeaveIP != nil {
t.Fatalf("a node that published nothing holds nothing: %+v %v", n, err)
}
}
func TestMarksAreDistinctAndStable(t *testing.T) {
var ns []Node
for _, name := range []string{"a", "b", "c", "d", "e", "f", "g", "h"} {
ns = append(ns, Node{Name: name})
}
first := assignMarks(ns)
seen := map[uint32]bool{}
for _, h := range first {
if seen[h.Mark] || h.Mark < 1 || h.Mark > MaxMarks {
t.Fatalf("bad or repeated mark: %+v", first)
}
seen[h.Mark] = true
}
again := assignMarks(ns)
for i := range first {
if first[i].Mark != again[i].Mark {
t.Fatalf("marks moved between passes")
}
}
}
func TestRenderedRules(t *testing.T) {
_, podNet, _ := net.ParseCIDR("10.32.0.0/12")
s := svc("gitea", "86.107.168.15")
sl := slice("gitea", map[string]bool{"10.40.0.7": true, "10.44.0.3": true, "10.44.0.4": true}, map[string]int32{"ssh": 2222, "https": 3000})
d := Compute("w1", nodes, []*coreapi.Service{s}, []*discovery.EndpointSlice{sl})
r := Render(d, EgressDesired{}, "w1", podNet, []string{"REWEAVE-LBS-DEADBEEF0000"})
for _, want := range []string{
"*nat\n:REWEAVE-LB - [0:0]\n:REWEAVE-LB-POST - [0:0]\n:REWEAVE-LB-CLASS - [0:0]\n",
// the client's address survives weave's masquerade; a pod's own connection (hairpin) is masqueraded here
"-A REWEAVE-LB-POST -m conntrack --ctstate DNAT --ctorigdst 86.107.168.15/32 -s 10.32.0.0/12 -j MASQUERADE\n",
"-A REWEAVE-LB-POST -m conntrack --ctstate DNAT --ctorigdst 86.107.168.15/32 -j ACCEPT\n",
":REWEAVE-LBS-DEADBEEF0000 - [0:0]\n", // stale chain emptied
"-A REWEAVE-LB -d 86.107.168.15/32 -p tcp -m tcp --dport 22 -m comment --comment \"gitea/gitea\" -j REWEAVE-LBS-",
"-p tcp -m statistic --mode random --probability 0.3333333333 -j DNAT --to-destination 10.40.0.7:2222\n",
"-p tcp -m statistic --mode random --probability 0.5000000000 -j DNAT --to-destination 10.44.0.3:2222\n",
"-p tcp -j DNAT --to-destination 10.44.0.4:2222\n",
"COMMIT\n",
} {
if !strings.Contains(r.Nat, want) {
t.Fatalf("nat lacks %q:\n%s", want, r.Nat)
}
}
for _, line := range strings.Split(r.Nat, "\n") {
// iptables refuses a DNAT to ip:port without the protocol in the same rule
if strings.Contains(line, "-j DNAT") && !strings.Contains(line, " -p tcp ") {
t.Fatalf("DNAT without its protocol: %q", line)
}
}
if strings.Count(r.Nat, "MASQUERADE") != 1 || strings.Contains(r.Nat, "SNAT") {
t.Fatalf("only a pod's hairpin connection may be masqueraded:\n%s", r.Nat)
}
if r.Mangle != "*mangle\n:REWEAVE-LB-MARK - [0:0]\n:REWEAVE-LB-MSS - [0:0]\nCOMMIT\n" {
t.Fatalf("a holder marks nothing:\n%s", r.Mangle)
}
d = Compute("w2", nodes, []*coreapi.Service{s}, []*discovery.EndpointSlice{sl})
r = Render(d, EgressDesired{}, "w2", podNet, nil)
m := d.Holders[0].Mark << 24
for _, want := range []string{
"-A REWEAVE-LB-MARK -m conntrack --ctdir REPLY -m connmark ! --mark 0x0/0xff000000 -j CONNMARK --restore-mark --nfmask 0xff000000 --ctmask 0xff000000\n",
"-A REWEAVE-LB-MARK -i weave -m mac --mac-source aa:aa:aa:aa:aa:02 ! -s 10.32.0.0/12 -d 10.32.0.0/12 -m conntrack --ctstate NEW -m comment --comment \"holder w1\" -j CONNMARK --set-xmark " + fmt.Sprintf("0x%x", m) + "/0xff000000\n",
} {
if !strings.Contains(r.Mangle, want) {
t.Fatalf("mangle lacks %q:\n%s", want, r.Mangle)
}
}
if r.Nat != "*nat\n:REWEAVE-LB - [0:0]\n:REWEAVE-LB-POST - [0:0]\n:REWEAVE-LB-CLASS - [0:0]\nCOMMIT\n" {
t.Fatalf("a non-holder forwards nothing:\n%s", r.Nat)
}
if r.Filter != "*filter\n:REWEAVE-LB-FWD - [0:0]\n:REWEAVE-LB-IN - [0:0]\nCOMMIT\n" {
t.Fatalf("no EgressService, no filter rules:\n%s", r.Filter)
}
}
+220
View File
@@ -0,0 +1,220 @@
package net
import (
"encoding/binary"
"errors"
"fmt"
"net"
"strings"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
// A pod's port is locked to the pod's own addresses: what the pod sends in through the host side of its veth has to
// carry the pod's MAC as its Ethernet source and be either IPv4 from the pod's address or ARP speaking for exactly
// that MAC and address (or probing, from 0.0.0.0). Everything else is dropped before the bridge sees it, so a pod can
// claim neither another pod's identity nor a node's, on its own node or, through weave, on any other.
//
// The lock is a set of tc filters on the veth's ingress qdisc. They match the way iproute2 lays out "ether src"
// (the Ethernet source at -8 and -4 from the network header) and "ip src" (at 12), and they go away with the veth.
const (
portLockPrioIPv4 = 1
portLockPrioARP = 2
portLockPrioProbe = 3
portLockPrioDrop = 9
)
var ingressParent = netlink.MakeHandle(0xffff, 0)
// PortLockFilters are the filters that lock the port with the given index to a pod's MAC and IPv4 address.
func PortLockFilters(index int, mac net.HardwareAddr, ip net.IP) ([]netlink.Filter, error) {
if len(mac) != 6 {
return nil, fmt.Errorf("port lock needs an Ethernet MAC, got %q", mac)
}
ip4 := ip.To4()
if ip4 == nil {
return nil, fmt.Errorf("port lock needs an IPv4 address, got %q", ip)
}
src := etherSource(mac)
arp := func(spa net.IP) []netlink.TcU32Key {
keys := append([]netlink.TcU32Key{}, src...)
return append(keys,
// Ethernet/IPv4 ARP: hardware type 1, protocol 0x0800, address lengths 6 and 4 (any operation)
netlink.TcU32Key{Mask: 0xffffffff, Val: 0x00010800, Off: 0},
netlink.TcU32Key{Mask: 0xffff0000, Val: 0x06040000, Off: 4},
// the sender hardware address (8..13) and the sender protocol address (14..17)
netlink.TcU32Key{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(mac[0:4]), Off: 8},
netlink.TcU32Key{Mask: 0xffffffff, Val: uint32(binary.BigEndian.Uint16(mac[4:6]))<<16 | uint32(binary.BigEndian.Uint16(spa[0:2])), Off: 12},
netlink.TcU32Key{Mask: 0xffff0000, Val: uint32(binary.BigEndian.Uint16(spa[2:4])) << 16, Off: 16},
)
}
ipv4 := append(append([]netlink.TcU32Key{}, src...), netlink.TcU32Key{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(ip4), Off: 12})
return []netlink.Filter{
portLockU32(index, portLockPrioIPv4, unix.ETH_P_IP, ipv4),
portLockU32(index, portLockPrioARP, unix.ETH_P_ARP, arp(ip4)),
portLockU32(index, portLockPrioProbe, unix.ETH_P_ARP, arp(net.IPv4zero.To4())),
&netlink.MatchAll{
FilterAttrs: netlink.FilterAttrs{LinkIndex: index, Parent: ingressParent, Priority: portLockPrioDrop, Protocol: unix.ETH_P_ALL},
Actions: []netlink.Action{gact(netlink.TC_ACT_SHOT)},
},
}, nil
}
// etherSource matches the Ethernet source address, which sits 8 bytes before the network header.
func etherSource(mac net.HardwareAddr) []netlink.TcU32Key {
return []netlink.TcU32Key{
{Mask: 0xffffffff, Val: binary.BigEndian.Uint32(mac[0:4]), Off: -8},
{Mask: 0xffff0000, Val: uint32(binary.BigEndian.Uint16(mac[4:6])) << 16, Off: -4},
}
}
func portLockU32(index int, prio uint16, proto uint16, keys []netlink.TcU32Key) *netlink.U32 {
// clipped: the vendored netlink serialises cap(Keys) keys, and spare capacity would go out as extra all-zero keys
keys = keys[:len(keys):len(keys)]
return &netlink.U32{
FilterAttrs: netlink.FilterAttrs{LinkIndex: index, Parent: ingressParent, Priority: prio, Protocol: proto},
Sel: &netlink.TcU32Sel{Flags: nl.TC_U32_TERMINAL, Keys: keys},
Actions: []netlink.Action{gact(netlink.TC_ACT_OK)},
}
}
func gact(action netlink.TcAct) netlink.Action {
return &netlink.GenericAction{ActionAttrs: netlink.ActionAttrs{Action: action}}
}
func ingressQdisc(index int) *netlink.Ingress {
return &netlink.Ingress{QdiscAttrs: netlink.QdiscAttrs{LinkIndex: index, Handle: ingressParent, Parent: netlink.HANDLE_INGRESS}}
}
// LockPort locks a pod's port (the host side of its veth) to the pod's MAC and IPv4 address. A port already locked
// to them is left alone; anything else on its ingress is replaced, and the pod's own traffic is let through before
// the rest is dropped, so a running pod never loses its own frames.
func LockPort(link netlink.Link, mac net.HardwareAddr, ip net.IP) error {
index := link.Attrs().Index
want, err := PortLockFilters(index, mac, ip)
if err != nil {
return err
}
if locked, _ := portHasFilters(link, want); locked {
return nil
}
// removing the ingress qdisc takes its filters with it
if err := netlink.QdiscDel(ingressQdisc(index)); err != nil && !errors.Is(err, unix.ENOENT) && !errors.Is(err, unix.EINVAL) {
return fmt.Errorf("clearing ingress of %s: %w", link.Attrs().Name, err)
}
if err := netlink.QdiscAdd(ingressQdisc(index)); err != nil {
return fmt.Errorf("adding ingress qdisc to %s: %w", link.Attrs().Name, err)
}
for _, f := range want {
if err := netlink.FilterAdd(f); err != nil {
return fmt.Errorf("adding port lock filter (prio %d) to %s: %w", f.Attrs().Priority, link.Attrs().Name, err)
}
}
return nil
}
// LockContainerPort locks the port of a pod attached by the CNI plugin: the host side of the veth whose container
// end is ifName in ns, to that end's MAC and the pod's address.
func LockContainerPort(ns netns.NsHandle, ifName string, ip net.IP) error {
var mac net.HardwareAddr
peer := 0
if err := WithNetNSLink(ns, ifName, func(link netlink.Link) error {
// a veth's link attribute is its peer's index, in the peer's namespace: the host's
mac, peer = link.Attrs().HardwareAddr, link.Attrs().ParentIndex
return nil
}); err != nil {
return err
}
if peer == 0 {
return fmt.Errorf("%s has no peer: not a veth", ifName)
}
host, err := netlink.LinkByIndex(peer)
if err != nil {
return fmt.Errorf("host side of %s: %w", ifName, err)
}
if host.Type() != "veth" || !strings.HasPrefix(host.Attrs().Name, vethPrefix+"pl") {
return fmt.Errorf("host side of %s is %s (%s), not a weave pod veth", ifName, host.Attrs().Name, host.Type())
}
return LockPort(host, mac, ip)
}
// PortLocked reports whether a port is locked to exactly this MAC and address.
func PortLocked(link netlink.Link, mac net.HardwareAddr, ip net.IP) (bool, error) {
want, err := PortLockFilters(link.Attrs().Index, mac, ip)
if err != nil {
return false, err
}
return portHasFilters(link, want)
}
func portHasFilters(link netlink.Link, want []netlink.Filter) (bool, error) {
have, err := netlink.FilterList(link, ingressParent)
if err != nil {
return false, err
}
var got []netlink.Filter
for _, f := range have {
// u32 lists the hash table that holds a filter as a filter of its own, without a selector
if u, ok := f.(*netlink.U32); ok && u.Sel == nil {
continue
}
got = append(got, f)
}
if len(got) != len(want) {
return false, nil
}
for _, w := range want {
found := false
for _, g := range got {
if sameFilter(g, w) {
found = true
break
}
}
if !found {
return false, nil
}
}
return true, nil
}
func sameFilter(a, b netlink.Filter) bool {
if a.Attrs().Priority != b.Attrs().Priority || a.Attrs().Protocol != b.Attrs().Protocol || a.Type() != b.Type() {
return false
}
switch fa := a.(type) {
case *netlink.U32:
fb := b.(*netlink.U32)
if fa.Sel == nil || fb.Sel == nil || len(fa.Sel.Keys) != len(fb.Sel.Keys) || fa.Sel.Flags&nl.TC_U32_TERMINAL == 0 {
return false
}
for i := range fa.Sel.Keys {
ka, kb := fa.Sel.Keys[i], fb.Sel.Keys[i]
if ka.Mask != kb.Mask || ka.Val != kb.Val || ka.Off != kb.Off {
return false
}
}
return sameActions(fa.Actions, fb.Actions)
case *netlink.MatchAll:
return sameActions(fa.Actions, b.(*netlink.MatchAll).Actions)
}
return false
}
func sameActions(a, b []netlink.Action) bool {
if len(a) != len(b) {
return false
}
for i := range a {
ga, ok1 := a[i].(*netlink.GenericAction)
gb, ok2 := b[i].(*netlink.GenericAction)
if !ok1 || !ok2 || ga.Attrs().Action != gb.Attrs().Action {
return false
}
}
return true
}
+228
View File
@@ -0,0 +1,228 @@
package net
import (
"bytes"
"encoding/binary"
"net"
"os"
"runtime"
"testing"
"time"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
"golang.org/x/sys/unix"
)
// TestPortLockInKernel builds a bridge with two pods in throwaway network namespaces, locks pod A's port and sends
// frames from A: what A may send reaches B, what claims anything but A's own addresses does not. It needs root.
func TestPortLockInKernel(t *testing.T) {
if os.Geteuid() != 0 {
t.Skip("needs root (creates network namespaces)")
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
orig, err := netns.Get()
if err != nil {
t.Fatal(err)
}
defer orig.Close()
defer netns.Set(orig)
hub, err := netns.New()
if err != nil {
t.Fatal(err)
}
defer hub.Close()
podA, _ := netns.New()
defer podA.Close()
podB, _ := netns.New()
defer podB.Close()
must(t, netns.Set(hub))
br := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: "br0"}}
must(t, netlink.LinkAdd(br))
must(t, netlink.LinkSetUp(br))
ipA, ipB := net.IPv4(10, 99, 0, 1).To4(), net.IPv4(10, 99, 0, 2).To4()
hostA, macA := attach(t, hub, podA, br, "vethwepltsta", ipA)
_, macB := attach(t, hub, podB, br, "vethwepltstb", ipB)
// as the CNI plugin does it: from the pod's end of the veth, in the pod's namespace
must(t, LockContainerPort(podA, "eth0", ipA))
if ok, err := PortLocked(hostA, macA, ipA); err != nil || !ok {
dumpIngress(t, hostA)
t.Fatalf("port not locked after LockContainerPort: %v %v", ok, err)
}
// locking again changes nothing (the filters are compared, not rebuilt)
must(t, LockPort(hostA, macA, ipA))
if ok, _ := PortLocked(hostA, macA, ipA); !ok {
t.Fatal("port lost its lock on a repeated LockPort")
}
send := socketIn(t, podA, "eth0")
recv := socketIn(t, podB, "eth0")
defer unix.Close(send)
defer unix.Close(recv)
other, _ := net.ParseMAC("02:00:00:00:00:99")
spoofIP := net.IPv4(10, 99, 0, 200).To4()
cases := []struct {
name string
frame []byte
passes bool
}{
{"ipv4 from its own addresses", ipv4Frame(macB, macA, ipA, ipB, "own-ip"), true},
{"ipv4 from another MAC", ipv4Frame(macB, other, ipA, ipB, "mac-spoof"), false},
{"ipv4 from another address", ipv4Frame(macB, macA, spoofIP, ipB, "ip-spoof"), false},
{"arp for itself", arpFrame(macA, macA, ipA, ipB, "own-arp"), true},
{"arp probe", arpFrame(macA, macA, net.IPv4zero.To4(), ipB, "probe"), true},
{"arp claiming another address", arpFrame(macA, macA, spoofIP, ipB, "arp-ip-spoof"), false},
{"arp claiming another MAC", arpFrame(macA, other, ipA, ipB, "arp-sha-spoof"), false},
{"arp from another MAC", arpFrame(other, other, ipA, ipB, "arp-eth-spoof"), false},
{"ipv6", etherFrame(macB, macA, 0x86dd, []byte("ipv6-frame")), false},
{"other ethertype", etherFrame(macB, macA, 0x88b5, []byte("experimental")), false},
}
for _, c := range cases {
drain(recv)
if _, err := unix.Write(send, c.frame); err != nil {
t.Fatalf("%s: send: %v", c.name, err)
}
got := sawPayload(recv, c.frame[len(c.frame)-12:], 300*time.Millisecond)
if got != c.passes {
t.Errorf("%s: reached B = %v, want %v", c.name, got, c.passes)
}
}
}
func dumpIngress(t *testing.T, link netlink.Link) {
have, _ := netlink.FilterList(link, netlink.MakeHandle(0xffff, 0))
for _, f := range have {
t.Logf("have %s %+v", f.Type(), f.Attrs())
if u, ok := f.(*netlink.U32); ok && u.Sel != nil {
t.Logf(" sel flags %x keys %+v actions %+v", u.Sel.Flags, u.Sel.Keys, u.Actions)
}
if m, ok := f.(*netlink.MatchAll); ok {
t.Logf(" actions %+v", m.Actions)
}
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
// attach plugs a pod namespace into the bridge through a veth named host on the hub side, eth0 on the pod side.
func attach(t *testing.T, hub, pod netns.NsHandle, br netlink.Link, host string, ip net.IP) (netlink.Link, net.HardwareAddr) {
t.Helper()
must(t, netlink.LinkAdd(&netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: host}, PeerName: "tmp" + host[len(host)-2:]}))
h, err := netlink.LinkByName(host)
must(t, err)
must(t, netlink.LinkSetMaster(h, br))
must(t, netlink.LinkSetUp(h))
p, err := netlink.LinkByName("tmp" + host[len(host)-2:])
must(t, err)
must(t, netlink.LinkSetNsFd(p, int(pod)))
must(t, netns.Set(pod))
p, err = netlink.LinkByName("tmp" + host[len(host)-2:])
must(t, err)
must(t, netlink.LinkSetName(p, "eth0"))
must(t, netlink.AddrAdd(p, &netlink.Addr{IPNet: &net.IPNet{IP: ip, Mask: net.CIDRMask(24, 32)}}))
must(t, netlink.LinkSetUp(p))
mac := p.Attrs().HardwareAddr
must(t, netns.Set(hub))
return h, mac
}
// socketIn opens a raw packet socket on an interface of a namespace (the socket stays in that namespace).
func socketIn(t *testing.T, ns netns.NsHandle, ifName string) int {
t.Helper()
cur, _ := netns.Get()
defer cur.Close()
must(t, netns.Set(ns))
defer netns.Set(cur)
link, err := netlink.LinkByName(ifName)
must(t, err)
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL)))
must(t, err)
must(t, unix.Bind(fd, &unix.SockaddrLinklayer{Protocol: htons(unix.ETH_P_ALL), Ifindex: link.Attrs().Index}))
return fd
}
func htons(v uint16) uint16 { return v<<8 | v>>8 }
func drain(fd int) {
buf := make([]byte, 2048)
for {
if _, _, err := unix.Recvfrom(fd, buf, unix.MSG_DONTWAIT); err != nil {
return
}
}
}
func sawPayload(fd int, marker []byte, within time.Duration) bool {
buf := make([]byte, 2048)
deadline := time.Now().Add(within)
for time.Now().Before(deadline) {
n, _, err := unix.Recvfrom(fd, buf, unix.MSG_DONTWAIT)
if err != nil {
time.Sleep(5 * time.Millisecond)
continue
}
if bytes.Contains(buf[:n], marker) {
return true
}
}
return false
}
func etherFrame(dst, src net.HardwareAddr, ethertype uint16, payload []byte) []byte {
f := make([]byte, 14, 14+len(payload)+12)
copy(f[0:6], dst)
copy(f[6:12], src)
binary.BigEndian.PutUint16(f[12:14], ethertype)
f = append(f, payload...)
// a fixed-size tail the receiver looks for, so each case is told apart
tail := make([]byte, 12)
copy(tail, payload)
return append(f, tail...)
}
func ipv4Frame(dst, src net.HardwareAddr, from, to net.IP, tag string) []byte {
body := []byte(tag)
h := make([]byte, 20)
h[0] = 0x45
binary.BigEndian.PutUint16(h[2:4], uint16(20+len(body)+12))
h[8] = 64
h[9] = 253 // experimental protocol: nothing answers it
copy(h[12:16], from)
copy(h[16:20], to)
binary.BigEndian.PutUint16(h[10:12], checksum(h))
return etherFrame(dst, src, unix.ETH_P_IP, append(h, body...))
}
func arpFrame(ethSrc, sha net.HardwareAddr, spa, tpa net.IP, tag string) []byte {
a := make([]byte, 28)
binary.BigEndian.PutUint16(a[0:2], 1)
binary.BigEndian.PutUint16(a[2:4], unix.ETH_P_IP)
a[4], a[5] = 6, 4
binary.BigEndian.PutUint16(a[6:8], 1) // request
copy(a[8:14], sha)
copy(a[14:18], spa)
copy(a[24:28], tpa)
bcast, _ := net.ParseMAC("ff:ff:ff:ff:ff:ff")
return etherFrame(bcast, ethSrc, unix.ETH_P_ARP, append(a, []byte(tag)...))
}
func checksum(b []byte) uint16 {
var s uint32
for i := 0; i+1 < len(b); i += 2 {
s += uint32(binary.BigEndian.Uint16(b[i:]))
}
for s>>16 != 0 {
s = s&0xffff + s>>16
}
return ^uint16(s)
}
+115
View File
@@ -0,0 +1,115 @@
package net
import (
"net"
"testing"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// The keys iproute2 writes for "u32 match ether src 02:11:22:33:44:55 match ip src 10.32.0.7/32" (tc filter show,
// iproute2 6.x): the port lock has to match the same bytes.
func TestPortLockMatchesIproute2Layout(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
fs, err := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
if err != nil {
t.Fatal(err)
}
if len(fs) != 4 {
t.Fatalf("want 4 filters, got %d", len(fs))
}
ipv4 := fs[0].(*netlink.U32)
want := []netlink.TcU32Key{
{Val: 0x02112233, Mask: 0xffffffff, Off: -8},
{Val: 0x44550000, Mask: 0xffff0000, Off: -4},
{Val: 0x0a200007, Mask: 0xffffffff, Off: 12},
}
if ipv4.Protocol != unix.ETH_P_IP || ipv4.Priority != portLockPrioIPv4 || ipv4.Sel.Flags&nl.TC_U32_TERMINAL == 0 {
t.Fatalf("ipv4 filter attrs: %+v flags %x", ipv4.FilterAttrs, ipv4.Sel.Flags)
}
assertKeys(t, "ipv4", ipv4.Sel.Keys, want)
for _, f := range fs {
if f.Attrs().LinkIndex != 7 || f.Attrs().Parent != netlink.MakeHandle(0xffff, 0) {
t.Fatalf("filter not on ingress of link 7: %+v", f.Attrs())
}
}
}
func TestPortLockARP(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
fs, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
common := []netlink.TcU32Key{
{Val: 0x02112233, Mask: 0xffffffff, Off: -8},
{Val: 0x44550000, Mask: 0xffff0000, Off: -4},
{Val: 0x00010800, Mask: 0xffffffff, Off: 0},
{Val: 0x06040000, Mask: 0xffff0000, Off: 4},
{Val: 0x02112233, Mask: 0xffffffff, Off: 8},
}
own := append(append([]netlink.TcU32Key{}, common...),
netlink.TcU32Key{Val: 0x44550a20, Mask: 0xffffffff, Off: 12}, // sha 44:55, spa 10.32
netlink.TcU32Key{Val: 0x00070000, Mask: 0xffff0000, Off: 16}, // spa .0.7
)
probe := append(append([]netlink.TcU32Key{}, common...),
netlink.TcU32Key{Val: 0x44550000, Mask: 0xffffffff, Off: 12},
netlink.TcU32Key{Val: 0x00000000, Mask: 0xffff0000, Off: 16},
)
arp, pr := fs[1].(*netlink.U32), fs[2].(*netlink.U32)
if arp.Protocol != unix.ETH_P_ARP || pr.Protocol != unix.ETH_P_ARP {
t.Fatal("ARP filters must be protocol arp")
}
assertKeys(t, "arp", arp.Sel.Keys, own)
assertKeys(t, "probe", pr.Sel.Keys, probe)
drop := fs[3].(*netlink.MatchAll)
if drop.Protocol != unix.ETH_P_ALL || drop.Priority != portLockPrioDrop {
t.Fatalf("drop filter attrs: %+v", drop.FilterAttrs)
}
if a := drop.Actions[0].(*netlink.GenericAction); a.Attrs().Action != netlink.TC_ACT_SHOT {
t.Fatalf("last filter must drop, has action %v", a.Attrs().Action)
}
for _, f := range fs[:3] {
if a := f.(*netlink.U32).Actions[0].(*netlink.GenericAction); a.Attrs().Action != netlink.TC_ACT_OK {
t.Fatalf("prio %d must pass, has action %v", f.Attrs().Priority, a.Attrs().Action)
}
}
}
func TestPortLockRefusesWhatItCannotPin(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
if _, err := PortLockFilters(7, mac, net.ParseIP("fd00::1")); err == nil {
t.Fatal("an IPv6 address must be refused: the lock pins IPv4 and ARP")
}
long, _ := net.ParseMAC("02:11:22:33:44:55:66:77")
if _, err := PortLockFilters(7, long, net.ParseIP("10.32.0.7")); err == nil {
t.Fatal("a non-Ethernet hardware address must be refused")
}
}
func TestSameFilterComparesKeysAndActions(t *testing.T) {
mac, _ := net.ParseMAC("02:11:22:33:44:55")
a, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.7"))
b, _ := PortLockFilters(7, mac, net.ParseIP("10.32.0.8"))
if !sameFilter(a[0], a[0]) || sameFilter(a[0], b[0]) {
t.Fatal("ipv4 filters for different addresses must differ")
}
if !sameFilter(a[3], b[3]) {
t.Fatal("the drop filters are the same for every pod")
}
if sameFilter(a[1], a[2]) {
t.Fatal("the ARP and probe filters differ in priority and keys")
}
}
func assertKeys(t *testing.T, what string, got, want []netlink.TcU32Key) {
t.Helper()
if len(got) != len(want) {
t.Fatalf("%s: want %d keys, got %d: %+v", what, len(want), len(got), got)
}
for i := range want {
if got[i].Val != want[i].Val || got[i].Mask != want[i].Mask || got[i].Off != want[i].Off {
t.Fatalf("%s key %d: want %08x/%08x at %d, got %08x/%08x at %d", what, i,
want[i].Val, want[i].Mask, want[i].Off, got[i].Val, got[i].Mask, got[i].Off)
}
}
}
+21
View File
@@ -1,6 +1,7 @@
package plugin
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
@@ -14,6 +15,7 @@ import (
current "github.com/containernetworking/cni/pkg/types/100"
"github.com/containernetworking/plugins/pkg/ipam"
weaveapi "github.com/rajch/weave/api"
"github.com/rajch/weave/lb/admit"
weavenet "github.com/rajch/weave/net"
ipamplugin "github.com/rajch/weave/plugin/ipam"
"github.com/vishvananda/netlink"
@@ -86,6 +88,18 @@ func (c *CNIPlugin) CmdAdd(args *skel.CmdArgs) error {
// Only expecting one address
ip := result.IPs[0]
// An EgressService member's first packet must already leave from its Service's address (or not at all):
// weave-npc installs the pod's egress rules before the pod has an interface. No answer, no pod (the
// runtime calls DEL, which releases the address, and kubelet retries).
if conf.EgressAdmission {
if ns, name := admit.PodFromArgs(args.Args); ns != "" && name != "" {
if _, err := admit.Admit(context.Background(), admit.SocketPath,
admit.Request{Namespace: ns, Name: name, IP: ip.Address.IP.String()}); err != nil {
return err
}
}
}
// If config says nothing about routes or gateway, default one will be via the bridge
if len(result.Routes) == 0 && ip.Gateway == nil {
bridgeIP, err := weavenet.FindBridgeIP(conf.BrName, &ip.Address)
@@ -147,6 +161,11 @@ func (c *CNIPlugin) CmdAdd(args *skel.CmdArgs) error {
}); err != nil {
return fmt.Errorf("error setting up routes: %s", err)
}
// the pod may only speak as itself: its port drops whatever claims another MAC or address. The pod has not
// started yet; if its port cannot be locked, it does not start.
if err := weavenet.LockContainerPort(ns, args.IfName, ip.Address.IP); err != nil {
return fmt.Errorf("error locking the pod's port: %s", err)
}
result.DNS = conf.DNS
return types.PrintResult(result, conf.CNIVersion)
@@ -241,4 +260,6 @@ type NetConf struct {
IPMasq bool `json:"ipMasq"`
MTU int `json:"mtu"`
HairpinMode bool `json:"hairpinMode"`
// EgressAdmission: weave-npc must admit every pod before it gets its interface (EgressService members)
EgressAdmission bool `json:"egressAdmission"`
}
+6 -1
View File
@@ -63,4 +63,9 @@ if ! mkdir -p $HOST_ROOT/opt/cni/bin ; then
fi
mkdir -p $HOST_ROOT/etc/cni/net.d
export HOST_ROOT
/home/weave/weave --local setup-cni
/home/weave/weave --local setup-cni
# Lock the ports of pods attached before the CNI plugin locked ports itself (it locks every pod it attaches from
# now on), from the runtime's record of each attachment. A port that cannot be locked stays as it was and is
# reported; weave starts regardless, since holding back the whole node's network would not make that port safer.
/usr/bin/weaveutil lock-ports "$HOST_ROOT/var/lib/cni/results" weave || echo "Some pods' ports could not be locked (see above)" >&2
+7 -4
View File
@@ -27,14 +27,17 @@ setup_iptables_backend() {
fi
fi
printf "iptables backend mode: %s\n" "$mode"
if [ "$mode" = "nft" ]; then
# The weave-kube image uses the iptables-nft tools as default
# from weave 2.9.0 onwards, but includes the legacy tools. If
# legacy is detected, change the default symlinks.
if [ "$mode" = "legacy" ]; then
[ -n "$WEAVE_DEBUG" ] && echo "Changing iptables symlinks..."
rm /sbin/iptables
rm /sbin/iptables-save
rm /sbin/iptables-restore
ln -s /sbin/iptables-nft /sbin/iptables
ln -s /sbin/iptables-nft-save /sbin/iptables-save
ln -s /sbin/iptables-nft-restore /sbin/iptables-restore
ln -s /sbin/iptables-legacy /sbin/iptables
ln -s /sbin/iptables-legacy-save /sbin/iptables-save
ln -s /sbin/iptables-legacy-restore /sbin/iptables-restore
fi
}
@@ -1,6 +1,68 @@
apiVersion: v1
kind: List
items:
# EgressService (reweave.levk.dev/v1alpha1), served by weave-npc's lb agent: a LoadBalancer Service's pods
# leave the cluster from the Service's address (named after the Service, same namespace)
- apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: egressservices.reweave.levk.dev
labels:
name: weave-net
spec:
group: reweave.levk.dev
scope: Namespaced
names:
plural: egressservices
singular: egressservice
kind: EgressService
listKind: EgressServiceList
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
additionalPrinterColumns:
- {name: Emit, type: string, jsonPath: .spec.emit}
- {name: Host, type: string, jsonPath: .status.host}
- {name: Address, type: string, jsonPath: .status.address}
- {name: Age, type: date, jsonPath: .metadata.creationTimestamp}
schema:
openAPIV3Schema:
type: object
description: >-
Makes every pod the same-named LoadBalancer Service (class reweave.levk.dev/node-address)
selects leave the cluster from that Service's address, or not at all.
required: [spec]
properties:
spec:
type: object
required: [emit]
properties:
sourceIPBy:
type: string
description: LoadBalancerIP - members leave from the Service's load-balancer address.
enum: [LoadBalancerIP]
default: LoadBalancerIP
emit:
type: string
description: Holder - members' packets are tunnelled to the node holding the address, which emits them.
enum: [Holder]
status:
type: object
properties:
host: {type: string}
address: {type: string}
blocks:
type: array
items:
type: object
required: [node, from, to]
properties:
node: {type: string}
from: {type: integer, format: int32}
to: {type: integer, format: int32}
- apiVersion: v1
kind: ServiceAccount
metadata:
@@ -48,6 +110,50 @@ items:
verbs:
- patch
- update
# weave-npc's LoadBalancer agent (lb): Services of class reweave.levk.dev/node-address
- apiGroups:
- ''
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- ''
resources:
- services/status
verbs:
- patch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- get
- list
- watch
# each node publishes the addresses it holds and its weave bridge (annotations)
- apiGroups:
- ''
resources:
- nodes
verbs:
- patch
- apiGroups:
- reweave.levk.dev
resources:
- egressservices
verbs:
- get
- list
- watch
- apiGroups:
- reweave.levk.dev
resources:
- egressservices/status
verbs:
- patch
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
@@ -139,6 +245,15 @@ items:
- name: xtables-lock
mountPath: /run/xtables.lock
readOnly: false
# the runtime's record of each pod attachment, and the pods' network namespaces it names: init
# locks the ports of pods attached before the CNI plugin locked ports itself
- name: cni-cache
mountPath: /host/var/lib/cni
readOnly: true
- name: netns
mountPath: /var/run/netns
mountPropagation: HostToContainer
readOnly: true
containers:
- name: weave
command:
@@ -196,6 +311,8 @@ items:
- name: xtables-lock
mountPath: /run/xtables.lock
readOnly: false
- name: weave-npc-run
mountPath: /run/weave-npc
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
hostPID: false
@@ -234,6 +351,18 @@ items:
hostPath:
path: /run/xtables.lock
type: FileOrCreate
- name: weave-npc-run
hostPath:
path: /run/weave-npc
type: DirectoryOrCreate
- name: cni-cache
hostPath:
path: /var/lib/cni
type: DirectoryOrCreate
- name: netns
hostPath:
path: /var/run/netns
type: DirectoryOrCreate
priorityClassName: system-node-critical
updateStrategy:
type: RollingUpdate
+11
View File
@@ -21,6 +21,7 @@ import (
"github.com/rajch/weave/common"
"github.com/rajch/weave/common/chains"
"github.com/rajch/weave/lb"
"github.com/rajch/weave/net"
"github.com/rajch/weave/net/ipset"
"github.com/rajch/weave/npc"
@@ -36,6 +37,7 @@ var (
nodeName string
maxList int
bridgePortName string
serveLB bool
)
func handleError(err error) {
@@ -337,6 +339,14 @@ func root(cmd *cobra.Command, args []string) {
go podController.Run(wait.NeverStop)
go npController.Run(wait.NeverStop)
if serveLB {
// LoadBalancer Services of class lb.Class: addresses this node holds, client address kept; and
// EgressServices: those Services' pods leave the cluster from that address (or not at all)
egress, err := lb.NewEgressClient(config)
handleFatal(err)
go lb.NewController(nodeName, client, egress, lb.NewDatapath(ipt)).Run(context.Background())
}
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
common.Log.Fatalf("Exiting: %v", <-signals)
@@ -354,6 +364,7 @@ func main() {
rootCmd.PersistentFlags().StringVar(&nodeName, "node-name", "", "only generate rules that apply to this node")
rootCmd.PersistentFlags().IntVar(&maxList, "max-list-size", 1024, "maximum size of ipset list (for namespaces)")
rootCmd.PersistentFlags().StringVar(&bridgePortName, "bridge-port-name", "vethwe-bridge", "name of the brige port on which packets are received and sent")
rootCmd.PersistentFlags().BoolVar(&serveLB, "lb", true, "serve LoadBalancer Services of class "+lb.Class+" from addresses nodes hold")
handleFatal(rootCmd.Execute())
}
+1
View File
@@ -42,6 +42,7 @@ func init() {
"rewrite-etc-hosts": rewriteEtcHosts,
"get-db-flag": getDBFlag,
"set-db-flag": setDBFlag,
"lock-ports": lockPorts,
}
}
+114
View File
@@ -0,0 +1,114 @@
package main
import (
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"github.com/vishvananda/netns"
weavenet "github.com/rajch/weave/net"
)
// lockPorts locks the ports of pods that were attached before the CNI plugin locked ports itself (it locks every
// pod it attaches). It works from the runtime's own record of each attachment, libcni's result cache: the pod's
// network namespace, the interface the plugin set up in it and the address it gave it. Ports already locked are
// left alone; pods whose namespace is gone are skipped.
func lockPorts(args []string) error {
if len(args) != 2 {
cmdUsage("lock-ports", "<cni-result-cache-dir> <network-name>")
}
dir, network := args[0], args[1]
entries, err := os.ReadDir(dir)
if os.IsNotExist(err) {
return nil // nothing attached yet
}
if err != nil {
return err
}
var locked, gone, failed int
for _, e := range entries {
if e.IsDir() || !strings.HasPrefix(e.Name(), network+"-") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, e.Name()))
if err != nil {
fmt.Fprintf(os.Stderr, "lock-ports: %s: %v\n", e.Name(), err)
failed++
continue
}
a, err := parseAttachment(data)
if err != nil {
fmt.Fprintf(os.Stderr, "lock-ports: %s: %v\n", e.Name(), err)
failed++
continue
}
if a.network != network {
continue
}
ns, err := netns.GetFromPath(a.netns)
if os.IsNotExist(err) {
gone++ // the pod is gone; the runtime has yet to clear its record
continue
}
if err != nil {
fmt.Fprintf(os.Stderr, "lock-ports: %s: namespace %s: %v\n", a.containerID, a.netns, err)
failed++
continue
}
err = weavenet.LockContainerPort(ns, a.ifName, a.ip)
ns.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "lock-ports: %s (%s): %v\n", a.containerID, a.ip, err)
failed++
continue
}
locked++
}
fmt.Printf("lock-ports: %d locked, %d gone, %d failed\n", locked, gone, failed)
if failed > 0 {
return fmt.Errorf("lock-ports: %d pods' ports not locked", failed)
}
return nil
}
type attachment struct {
network, containerID, netns, ifName string
ip net.IP
}
// parseAttachment reads one libcni cache entry ("cniCacheV1"): which network attached which container, in which
// namespace, through which interface, with which IPv4 address.
func parseAttachment(data []byte) (attachment, error) {
var c struct {
Kind string `json:"kind"`
ContainerID string `json:"containerId"`
IfName string `json:"ifName"`
NetworkName string `json:"networkName"`
NetNS string `json:"netns"`
Result struct {
IPs []struct {
Address string `json:"address"`
} `json:"ips"`
} `json:"result"`
}
if err := json.Unmarshal(data, &c); err != nil {
return attachment{}, err
}
if c.Kind != "cniCacheV1" {
return attachment{}, fmt.Errorf("unknown cache entry kind %q", c.Kind)
}
if c.NetNS == "" || c.IfName == "" {
return attachment{}, fmt.Errorf("cache entry for %s names no namespace or interface", c.ContainerID)
}
for _, a := range c.Result.IPs {
ip, _, err := net.ParseCIDR(a.Address)
if err == nil && ip.To4() != nil {
return attachment{network: c.NetworkName, containerID: c.ContainerID, netns: c.NetNS, ifName: c.IfName, ip: ip.To4()}, nil
}
}
return attachment{}, fmt.Errorf("cache entry for %s has no IPv4 address", c.ContainerID)
}
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"net"
"testing"
)
// shaped like CRI-O's libcni cache on the SG nodes (cniCacheV1, CNI 1.0.0 result); the config is elided
const cacheEntry = `{"kind":"cniCacheV1","containerId":"2b61982132d6039b210e2fa5896555c8a921a07c756bd8753963440e07dc5f98",
"config":"eyJ9","ifName":"eth0","networkName":"weave","netns":"/var/run/netns/8828bbba-9a68-460c-980b-266dc3c9342c",
"cniArgs":[["K8S_POD_NAMESPACE","default"]],"capabilityArgs":{},
"result":{"cniVersion":"1.0.0","ips":[{"address":"10.44.0.12/12","gateway":"10.44.0.0"}]}}`
func TestParseAttachment(t *testing.T) {
a, err := parseAttachment([]byte(cacheEntry))
if err != nil {
t.Fatal(err)
}
if a.network != "weave" || a.ifName != "eth0" || a.netns != "/var/run/netns/8828bbba-9a68-460c-980b-266dc3c9342c" ||
!a.ip.Equal(net.ParseIP("10.44.0.12")) || a.containerID[:7] != "2b61982" {
t.Fatalf("parsed %+v", a)
}
}
func TestParseAttachmentRefusesWhatItCannotLock(t *testing.T) {
for name, entry := range map[string]string{
"other kind": `{"kind":"cniCacheV2","netns":"/x","ifName":"eth0","result":{"ips":[{"address":"10.0.0.1/8"}]}}`,
"no netns": `{"kind":"cniCacheV1","ifName":"eth0","result":{"ips":[{"address":"10.0.0.1/8"}]}}`,
"no ipv4": `{"kind":"cniCacheV1","netns":"/x","ifName":"eth0","result":{"ips":[{"address":"fd00::1/64"}]}}`,
"not json": `{`,
"no addresses": `{"kind":"cniCacheV1","netns":"/x","ifName":"eth0","result":{}}`,
} {
if _, err := parseAttachment([]byte(entry)); err == nil {
t.Errorf("%s: want an error", name)
}
}
}
+66 -1
View File
@@ -1,6 +1,71 @@
# Changelog
All changes made to the weave net codebase during the reweave effort will be documented in this file.
All changes made to the weave net codebase since the fork will be documented in this file.
## 2.9.0
### Changed
* Changed version in `reweave/Makefile` to 2.9.0
* Modified reweave and main CHANGELOG.md
## 2.9.0-beta1
From this version onwards, we will assume that the default backend for `iptables` is `iptables-nft`. For backward compatibility, we will include `iptables-legacy` in the weave images, and provide a way to use that if required.
### Changed
* Upgraded golang.org/x/crypto to v0.31.1
* Upgraded golang.org/x/net to v0.33.0
* Which upgraded:
golang.org/x/mod v0.17.0
golang.org/x/sync v0.10.0
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
* Ran `go mod tidy` and `go mod vendor`
### Added
* The Dockerfile was modified to keep the default iptables backend (nft) for all Alpine-based images, and to include the legacy backend but not set it as default.
* The `weave` script was modified to accept and propogate a new environment variable called `IPTABLES_BACKEND`, whose default value is `nft`. If its value is set to `legacy`, the script will use the command `iptables-legacy` instead of `iptables` when invoking that tool.
* The `launch.sh` script, which is the entry point for the `weave-kube` and `weave-npc` images, was modified to assume the `nft` backend as default, and to change the backend only if legacy mode was autodiscovered or set via the existing environment variable `IPTABLES_BACKEND`.
## 2.8.10
### Changed
* Changed version in `reweave/Makefile` to 2.8.10
* Modified reweave and main CHANGELOG.md
## 2.8.10-beta2
### Changed
* Changed go version in `go.mod` to 1.23.1
* Changed go base image in `reweave/build/Dockerfile` to `golang:1.23.1-bullseye`.
## 2.8.10-beta1
### Changed
* Changed the Alpine base image in `reweave/Makefile` and `reweave/build/Dockerfile` to `alpine:3.20.3`
## 2.8.9
### Changed
* Changed version in `reweave/Makefile` to 2.8.9
* Modified reweave and main CHANGELOG.md
## 2.8.9-beta1
### Changed
* Changed go base image in `reweave/build/Dockerfile` to `golang:1.22.2-bullseye`.
* Changed the Alpine base image in `reweave/Makefile` and `reweave/build/Dockerfile` to `alpine:3.20.2`
* Upgraded github.com/docker/docker to v25.0.6+incompatible
* Ran `go mod tidy` and `go mod vendor`
## 2.8.8
+2 -2
View File
@@ -1,7 +1,7 @@
IMAGE_VERSION ?= 2.8.8
IMAGE_VERSION ?= 2.9.0
REGISTRY_USER ?= rajchaudhuri
ALPINE_BASEIMAGE ?= alpine:3.20.0
ALPINE_BASEIMAGE ?= alpine:3.20.3
# Targets
.PHONY: build-images
+21 -14
View File
@@ -1,13 +1,13 @@
# syntax=docker/dockerfile:1
ARG ALPINE_BASEIMAGE=alpine:3.19.1
ARG ALPINE_BASEIMAGE=alpine:3.20.3
########
# Stage 1: go 1.20 on debian bullseye
# Stage 1: go 1.23.1 on debian bullseye
# This installs cross-compilation tools for supported architectures.
# It will be built only once for a given build platform. Subsequent
# stages will start from this and cross-compile for each target
# architecture.
FROM --platform=${BUILDPLATFORM} golang:1.21.9-bullseye AS builderbase
FROM --platform=${BUILDPLATFORM} golang:1.23.1-bullseye AS builderbase
# Support Raspberry Pi 2 and newer
ENV GOARM 7
@@ -32,6 +32,9 @@ ENV DEB_CROSSPLATFORMS \
RUN <<EOCROSSCOMPILERS
set -e
echo 'Acquire::Retries "6";' > /etc/apt/apt.conf.d/80-retries
# bullseye (this base) and buster (libpcap below) are archived: deb.debian.org
# no longer serves all their packages, archive.debian.org does
sed -i 's|http://deb.debian.org|http://archive.debian.org|g' /etc/apt/sources.list
for platform in ${DEB_CROSSPLATFORMS}; do dpkg --add-architecture $platform; done
apt-get update
apt-get install -y build-essential
@@ -48,8 +51,8 @@ EOCROSSCOMPILERS
# Install libpcap-dev version 1.8.1-6+deb10u1 for all supported
# architectures.
RUN <<EOBUSTERREPOS
echo "deb http://deb.debian.org/debian/ buster main" > /etc/apt/sources.list.d/busters-repos.list
echo "deb-src http://deb.debian.org/debian/ buster main" >> /etc/apt/sources.list.d/busters-repos.list
echo "deb http://archive.debian.org/debian/ buster main" > /etc/apt/sources.list.d/busters-repos.list
echo "deb-src http://archive.debian.org/debian/ buster main" >> /etc/apt/sources.list.d/busters-repos.list
apt-get update
for platform in ${DEB_CROSSPLATFORMS}; do apt-get install -y libpcap0.8:${platform}=1.8.1-6+deb10u1 libpcap0.8-dev:${platform}=1.8.1-6+deb10u1 libpcap-dev:${platform}=1.8.1-6+deb10u1; done
EOBUSTERREPOS
@@ -86,7 +89,7 @@ EOMAKE
# Stage 3: Alpine Base
# This stage exists in case there are any additional tweaks
# required in the alpine base image.
FROM --platform=linux/${TARGETARCH} ${ALPINE_BASEIMAGE} as alpinebase
FROM --platform=linux/${TARGETARCH} ${ALPINE_BASEIMAGE} AS alpinebase
# Any additional tweaks will appear here
# Last update on top of alpine:3.19.1
RUN apk update \
@@ -124,11 +127,13 @@ RUN <<EOWEAVER
bind-tools \
ca-certificates
# Alpine 3.19 made nftables the default backend for iptables
# For backward compatibility, the following is required:
# From weave version 2.9.0 onwards, weave will also assume
# the same, but will include iptables-legacy for backward
# compatibility.
apk add iptables-legacy
ln -sf /sbin/iptables-legacy /sbin/iptables
ln -sf /sbin/iptables-legacy-save /sbin/iptables-save
ln -sf /sbin/iptables-legacy-restore /sbin/iptables-restore
# ln -sf /sbin/iptables-legacy /sbin/iptables
# ln -sf /sbin/iptables-legacy-save /sbin/iptables-save
# ln -sf /sbin/iptables-legacy-restore /sbin/iptables-restore
rm -rf /var/cache/apk/*
EOWEAVER
@@ -218,11 +223,13 @@ RUN <<EONPC
mknod /var/log/ulogd.pcap p
# Alpine 3.19 made nftables the default backend for iptables
# For backward compatibility, the following is required:
# From weave version 2.9.0 onwards, weave will also assume
# the same, but will include iptables-legacy for backward
# compatibility.
apk add iptables-legacy
ln -sf /sbin/iptables-legacy /sbin/iptables
ln -sf /sbin/iptables-legacy-save /sbin/iptables-save
ln -sf /sbin/iptables-legacy-restore /sbin/iptables-restore
# ln -sf /sbin/iptables-legacy /sbin/iptables
# ln -sf /sbin/iptables-legacy-save /sbin/iptables-save
# ln -sf /sbin/iptables-legacy-restore /sbin/iptables-restore
rm -rf /var/cache/apk/*
EONPC
+1 -1
View File
@@ -1 +1 @@
{"schemaVersion": 1, "label": "Vulnerabilty count", "message": "3", "color": "orange"}
{"schemaVersion": 1, "label": "Vulnerabilty count", "message": "52", "color": "orange"}
+192 -30
View File
@@ -1,61 +1,223 @@
# Vulnerability Report
```
Report date: 2024-06-19
Unique vulnerability count: 3
Images version: 2.8.8
Report date: 2025-09-16
Unique vulnerability count: 52
Images version: 2.9.0
```
## Scanner Details
```
Application: grype
Version: 0.79.1
BuildDate: 2024-06-17T19:33:32Z
GitCommit: 239741f535c59d6e1b9faee61f64ebcf4361d2c5
GitDescription: v0.79.1
Version: 0.100.0
BuildDate: 2025-09-15T21:51:57Z
GitCommit: 088112b26e638c139a513f387f7a6e51f1a8b76d
GitDescription: v0.100.0
Platform: linux/amd64
GoVersion: go1.22.4
GoVersion: go1.24.7
Compiler: gc
Syft Version: v1.7.0
Supported DB Schema: 5
Syft Version: v1.33.0
Supported DB Schema: 6
```
## Vulnerabilities
### weave-kube: (3)
### weave-kube: (51)
```
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
stdlib go1.21.9 go-module CVE-2024-24790 Critical
stdlib go1.21.9 go-module CVE-2024-24789 Medium
stdlib go1.21.9 go-module CVE-2024-24787 Unknown
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libxml2 2.12.7-r0 apk CVE-2024-40896 Critical 0.4% (58th) 0.3
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
libcrypto3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
libssl3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
xz-libs 5.6.2-r0 5.6.2-r1 apk CVE-2025-31115 High 0.2% (39th) 0.1
golang.org/x/crypto v0.31.0 0.35.0 go-module GHSA-hcg3-q754-cr77 High 0.2% (40th) 0.1
c-ares 1.33.1-r0 apk CVE-2025-31498 High 0.1% (29th) < 0.1
golang.org/x/oauth2 v0.10.0 0.27.0 go-module GHSA-6v2p-p543-phr9 High < 0.1% (23rd) < 0.1
libxml2 2.12.7-r0 2.12.7-r2 apk CVE-2025-27113 High < 0.1% (18th) < 0.1
stdlib go1.23.1 *1.23.12, 1.24.6 go-module CVE-2025-47907 High < 0.1% (18th) < 0.1
libcrypto3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
libssl3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
curl 8.11.1-r0 apk CVE-2025-9086 High < 0.1% (10th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45336 Medium < 0.1% (14th) < 0.1
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32414 High < 0.1% (7th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45341 Medium < 0.1% (9th) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32415 High < 0.1% (5th) < 0.1
curl 8.11.1-r0 apk CVE-2025-10148 Medium < 0.1% (5th) < 0.1
bind-libs 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
bind-tools 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
stdlib go1.23.1 *1.23.8, 1.24.2 go-module CVE-2025-22871 Critical < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.10, 1.24.4 go-module CVE-2025-4673 Medium < 0.1% (3rd) < 0.1
golang.org/x/net v0.33.0 0.38.0 go-module GHSA-vvgc-356p-c3xw Medium < 0.1% (4th) < 0.1
curl 8.11.1-r0 apk CVE-2025-4947 Medium < 0.1% (3rd) < 0.1
musl 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
musl-utils 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
curl 8.11.1-r0 apk CVE-2025-5025 Medium < 0.1% (3rd) < 0.1
busybox 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
bind-libs 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
bind-tools 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
golang.org/x/net v0.33.0 0.36.0 go-module GHSA-qxp5-gwg8-xv66 Medium < 0.1% (2nd) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2024-56171 High < 0.1% (0th) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2025-24928 High < 0.1% (0th) < 0.1
busybox 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
github.com/containerd/containerd v1.7.11 1.7.27 go-module GHSA-265r-hfxg-fhmg Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 1.22.12, *1.23.6, 1.24.0-rc.3 go-module CVE-2025-22866 Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.11, 1.24.5 go-module CVE-2025-4674 High < 0.1% (0th) < 0.1
github.com/docker/docker v25.0.6+incompatible go-module GHSA-4vq8-7jfc-9cvp Low < 0.1% (1st) < 0.1
```
### weave-npc: (3)
### weave-npc: (23)
```
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
stdlib go1.21.9 go-module CVE-2024-24790 Critical
stdlib go1.21.9 go-module CVE-2024-24789 Medium
stdlib go1.21.9 go-module CVE-2024-24787 Unknown
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
libcrypto3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
libssl3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
golang.org/x/crypto v0.31.0 0.35.0 go-module GHSA-hcg3-q754-cr77 High 0.2% (40th) 0.1
golang.org/x/oauth2 v0.10.0 0.27.0 go-module GHSA-6v2p-p543-phr9 High < 0.1% (23rd) < 0.1
stdlib go1.23.1 *1.23.12, 1.24.6 go-module CVE-2025-47907 High < 0.1% (18th) < 0.1
libcrypto3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
libssl3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45336 Medium < 0.1% (14th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45341 Medium < 0.1% (9th) < 0.1
stdlib go1.23.1 *1.23.8, 1.24.2 go-module CVE-2025-22871 Critical < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.10, 1.24.4 go-module CVE-2025-4673 Medium < 0.1% (3rd) < 0.1
golang.org/x/net v0.33.0 0.38.0 go-module GHSA-vvgc-356p-c3xw Medium < 0.1% (4th) < 0.1
musl 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
musl-utils 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
busybox 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
golang.org/x/net v0.33.0 0.36.0 go-module GHSA-qxp5-gwg8-xv66 Medium < 0.1% (2nd) < 0.1
busybox 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
stdlib go1.23.1 1.22.12, *1.23.6, 1.24.0-rc.3 go-module CVE-2025-22866 Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.11, 1.24.5 go-module CVE-2025-4674 High < 0.1% (0th) < 0.1
```
### weave: (3)
### weave: (50)
```
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
stdlib go1.21.9 go-module CVE-2024-24790 Critical
stdlib go1.21.9 go-module CVE-2024-24789 Medium
stdlib go1.21.9 go-module CVE-2024-24787 Unknown
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libxml2 2.12.7-r0 apk CVE-2024-40896 Critical 0.4% (58th) 0.3
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
libcrypto3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
libssl3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
xz-libs 5.6.2-r0 5.6.2-r1 apk CVE-2025-31115 High 0.2% (39th) 0.1
golang.org/x/crypto v0.31.0 0.35.0 go-module GHSA-hcg3-q754-cr77 High 0.2% (40th) 0.1
c-ares 1.33.1-r0 apk CVE-2025-31498 High 0.1% (29th) < 0.1
libxml2 2.12.7-r0 2.12.7-r2 apk CVE-2025-27113 High < 0.1% (18th) < 0.1
stdlib go1.23.1 *1.23.12, 1.24.6 go-module CVE-2025-47907 High < 0.1% (18th) < 0.1
libcrypto3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
libssl3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
curl 8.11.1-r0 apk CVE-2025-9086 High < 0.1% (10th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45336 Medium < 0.1% (14th) < 0.1
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32414 High < 0.1% (7th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45341 Medium < 0.1% (9th) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32415 High < 0.1% (5th) < 0.1
curl 8.11.1-r0 apk CVE-2025-10148 Medium < 0.1% (5th) < 0.1
bind-libs 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
bind-tools 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
stdlib go1.23.1 *1.23.8, 1.24.2 go-module CVE-2025-22871 Critical < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.10, 1.24.4 go-module CVE-2025-4673 Medium < 0.1% (3rd) < 0.1
golang.org/x/net v0.33.0 0.38.0 go-module GHSA-vvgc-356p-c3xw Medium < 0.1% (4th) < 0.1
curl 8.11.1-r0 apk CVE-2025-4947 Medium < 0.1% (3rd) < 0.1
musl 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
musl-utils 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
curl 8.11.1-r0 apk CVE-2025-5025 Medium < 0.1% (3rd) < 0.1
busybox 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
bind-libs 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
bind-tools 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
golang.org/x/net v0.33.0 0.36.0 go-module GHSA-qxp5-gwg8-xv66 Medium < 0.1% (2nd) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2024-56171 High < 0.1% (0th) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2025-24928 High < 0.1% (0th) < 0.1
busybox 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
github.com/containerd/containerd v1.7.11 1.7.27 go-module GHSA-265r-hfxg-fhmg Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 1.22.12, *1.23.6, 1.24.0-rc.3 go-module CVE-2025-22866 Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.11, 1.24.5 go-module CVE-2025-4674 High < 0.1% (0th) < 0.1
github.com/docker/docker v25.0.6+incompatible go-module GHSA-4vq8-7jfc-9cvp Low < 0.1% (1st) < 0.1
```
### weaveexec: (3)
### weaveexec: (50)
```
NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY
stdlib go1.21.9 go-module CVE-2024-24790 Critical
stdlib go1.21.9 go-module CVE-2024-24789 Medium
stdlib go1.21.9 go-module CVE-2024-24787 Unknown
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0665 Critical 4.0% (87th) 3.8
libxml2 2.12.7-r0 apk CVE-2024-40896 Critical 0.4% (58th) 0.3
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0725 High 0.4% (61st) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-11187 High 0.4% (57th) 0.3
bind-libs 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
bind-tools 9.18.32-r0 9.18.33-r0 apk CVE-2024-12705 High 0.3% (49th) 0.2
libcrypto3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
libssl3 3.3.2-r1 3.3.3-r0 apk CVE-2024-12797 Medium 0.3% (49th) 0.1
xz-libs 5.6.2-r0 5.6.2-r1 apk CVE-2025-31115 High 0.2% (39th) 0.1
golang.org/x/crypto v0.31.0 0.35.0 go-module GHSA-hcg3-q754-cr77 High 0.2% (40th) 0.1
c-ares 1.33.1-r0 apk CVE-2025-31498 High 0.1% (29th) < 0.1
libxml2 2.12.7-r0 2.12.7-r2 apk CVE-2025-27113 High < 0.1% (18th) < 0.1
stdlib go1.23.1 *1.23.12, 1.24.6 go-module CVE-2025-47907 High < 0.1% (18th) < 0.1
libcrypto3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
libssl3 3.3.2-r1 3.3.2-r2 apk CVE-2024-13176 Medium < 0.1% (24th) < 0.1
curl 8.11.1-r0 apk CVE-2025-9086 High < 0.1% (10th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45336 Medium < 0.1% (14th) < 0.1
curl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libcurl 8.11.1-r0 8.12.0-r0 apk CVE-2025-0167 Low < 0.1% (23rd) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32414 High < 0.1% (7th) < 0.1
stdlib go1.23.1 1.22.11, *1.23.5, 1.24.0-rc.2 go-module CVE-2024-45341 Medium < 0.1% (9th) < 0.1
libxml2 2.12.7-r0 2.12.10-r0 apk CVE-2025-32415 High < 0.1% (5th) < 0.1
curl 8.11.1-r0 apk CVE-2025-10148 Medium < 0.1% (5th) < 0.1
bind-libs 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
bind-tools 9.18.32-r0 9.18.37-r0 apk CVE-2025-40775 High < 0.1% (2nd) < 0.1
stdlib go1.23.1 *1.23.8, 1.24.2 go-module CVE-2025-22871 Critical < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.10, 1.24.4 go-module CVE-2025-4673 Medium < 0.1% (3rd) < 0.1
golang.org/x/net v0.33.0 0.38.0 go-module GHSA-vvgc-356p-c3xw Medium < 0.1% (4th) < 0.1
curl 8.11.1-r0 apk CVE-2025-4947 Medium < 0.1% (3rd) < 0.1
musl 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
musl-utils 1.2.5-r0 1.2.5-r1 apk CVE-2025-26519 High < 0.1% (1st) < 0.1
curl 8.11.1-r0 apk CVE-2025-5025 Medium < 0.1% (3rd) < 0.1
busybox 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2024-58251 Low < 0.1% (7th) < 0.1
bind-libs 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
bind-tools 9.18.32-r0 apk CVE-2025-40776 High < 0.1% (0th) < 0.1
golang.org/x/net v0.33.0 0.36.0 go-module GHSA-qxp5-gwg8-xv66 Medium < 0.1% (2nd) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2024-56171 High < 0.1% (0th) < 0.1
libxml2 2.12.7-r0 2.12.7-r1 apk CVE-2025-24928 High < 0.1% (0th) < 0.1
busybox 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
busybox-binsh 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
ssl_client 1.36.1-r29 apk CVE-2025-46394 Low < 0.1% (4th) < 0.1
github.com/containerd/containerd v1.7.11 1.7.27 go-module GHSA-265r-hfxg-fhmg Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 1.22.12, *1.23.6, 1.24.0-rc.3 go-module CVE-2025-22866 Medium < 0.1% (1st) < 0.1
stdlib go1.23.1 *1.23.11, 1.24.5 go-module CVE-2025-4674 High < 0.1% (0th) < 0.1
github.com/docker/docker v25.0.6+incompatible go-module GHSA-4vq8-7jfc-9cvp Low < 0.1% (1st) < 0.1
```
+76
View File
@@ -0,0 +1,76 @@
# End-to-end test of EgressService (weave-npc's lb agent). Placeholders are filled by run.sh: ADDR (an
# address HOLDER holds), HOLDER, REMOTE and OTHER (the cluster's other two nodes).
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Namespace
metadata:
name: egtest
- apiVersion: v1
kind: Service
metadata:
name: mail
namespace: egtest
spec:
type: LoadBalancer
loadBalancerClass: reweave.levk.dev/node-address
loadBalancerIP: ADDR
allocateLoadBalancerNodePorts: false
selector: {egress: member}
ports: [{name: probe, port: 18090, targetPort: 8080}]
- apiVersion: reweave.levk.dev/v1alpha1
kind: EgressService
metadata:
name: mail
namespace: egtest
spec:
emit: Holder
# members on every node, and one that is not a member
- apiVersion: v1
kind: Pod
metadata: {name: member-remote, namespace: egtest, labels: {egress: member}}
spec:
nodeName: REMOTE
containers:
# root + NET_RAW: ping and traceroute need raw sockets (the image runs as an unprivileged user)
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"], securityContext: {runAsUser: 0, capabilities: {add: [NET_RAW]}}}
- apiVersion: v1
kind: Pod
metadata: {name: member-other, namespace: egtest, labels: {egress: member}}
spec:
nodeName: OTHER
containers:
# root + NET_RAW: ping and traceroute need raw sockets (the image runs as an unprivileged user)
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"], securityContext: {runAsUser: 0, capabilities: {add: [NET_RAW]}}}
- apiVersion: v1
kind: Pod
metadata: {name: member-holder, namespace: egtest, labels: {egress: member}}
spec:
nodeName: HOLDER
containers:
# root + NET_RAW: ping and traceroute need raw sockets (the image runs as an unprivileged user)
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"], securityContext: {runAsUser: 0, capabilities: {add: [NET_RAW]}}}
- apiVersion: v1
kind: Pod
metadata: {name: member-dns, namespace: egtest, labels: {egress: member}}
spec:
nodeName: REMOTE
containers: [{name: c, image: "registry.k8s.io/e2e-test-images/jessie-dnsutils:1.7", command: ["sleep", "3600"]}]
- apiVersion: v1
kind: Pod
metadata: {name: plain, namespace: egtest, labels: {egress: "none"}}
spec:
nodeName: REMOTE
containers: [{name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"]}]
# east-west: /clientip answers with the address the connection came from
- apiVersion: v1
kind: Pod
metadata: {name: echo, namespace: egtest, labels: {app: echo}}
spec:
nodeName: OTHER
containers:
- name: echo
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
readinessProbe: {httpGet: {path: /healthz, port: 8080}}
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# End-to-end test of EgressService against a live cluster (assertions A1-A6 of the design, plus the first
# packet). Members ask outside services which address they come from: api.ipify.org (TCP) and OpenDNS's
# myip.opendns.com (UDP).
#
# CP="ssh ... root@<cp>" HOLDER_SSH="ssh ... root@<holder>" REMOTE_SSH="ssh ... root@<remote>" \
# ADDR=<address HOLDER holds> HOLDER=<node> REMOTE=<node> OTHER=<node> \
# reweave/test/egress/run.sh
#
# Asserts, in order:
# 0. the holder writes the status: host, address, a port block for every node
# 1. the first packet of a member started after the EgressService leaves as ADDR (CNI admission)
# A1/A2. members on every node leave as ADDR (TCP and UDP) and get their replies; ping works from every node
# ICMP errors from routers on the way (traceroute's time-exceeded) reach a member on another node
# A4. a non-member pod on the same node as a member leaves as its node (untouched), and no rule names it
# A3. a member talking to a pod sees its own pod address there (east-west untouched)
# A5. naming an address nobody holds clears the status, and members' outside traffic (TCP, ping) is dropped
# A6. deleting the EgressService removes its state: members leave as their node again, rules are gone
# Leaves nothing behind (namespace egtest is deleted, also on failure).
set -euo pipefail
: "${CP:?}" "${HOLDER_SSH:?}" "${REMOTE_SSH:?}" "${ADDR:?}" "${HOLDER:?}" "${REMOTE:?}" "${OTHER:?}"
here=$(cd "$(dirname "$0")" && pwd)
k() { $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl $*"; }
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo "ok: $*"; }
cleanup() { k delete namespace egtest --ignore-not-found --wait=false >/dev/null 2>&1 || true; }
trap cleanup EXIT
eventually() {
local n=$1; shift
for _ in $(seq "$n"); do
if "$@" >/dev/null 2>&1; then return 0; fi
sleep 1
done
return 1
}
whoami_tcp() { k -n egtest exec "$1" -- curl -s --max-time 10 https://api.ipify.org; }
# each check re-reads on every try of eventually
tcp_is() { [ "$(whoami_tcp "$1" || true)" = "$2" ]; }
whoami_udp() { k -n egtest exec "$1" -- dig +short +time=5 +tries=1 myip.opendns.com @208.67.222.222; }
node_ip() { k get node "$1" -o "jsonpath='{.status.addresses[?(@.type==\"InternalIP\")].address}'"; }
remote_ip=$(node_ip "$REMOTE")
[ -n "$remote_ip" ] || fail "no address for $REMOTE"
sed -e "s/\bADDR\b/$ADDR/g; s/\bHOLDER\b/$HOLDER/g; s/\bREMOTE\b/$REMOTE/g; s/\bOTHER\b/$OTHER/g" \
"$here/egress-test.yaml" | $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f -" >/dev/null
k -n egtest wait --for=condition=Ready pod --all --timeout=300s >/dev/null || fail "test pods not ready"
status() { k -n egtest get egressservice mail -o "jsonpath='{.status.host} {.status.address} {.status.blocks[*].node}'"; }
status_is() { [ "$(status)" = "$1" ]; }
first_line() { k -n egtest logs member-first 2>/dev/null | head -1; }
said_something() { [ -n "$(first_line)" ]; }
want="$HOLDER $ADDR $(printf '%s\n' "$HOLDER" "$REMOTE" "$OTHER" | sort | tr '\n' ' ' | sed 's/ $//')"
eventually 60 status_is "$want" || fail "status: '$(status)', want '$want'"
ok "0. status: host $HOLDER, address $ADDR, a port block for each node"
# a member created now: its very first connection must already leave as ADDR
$CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl -n egtest run member-first --labels egress=member --restart=Never \
--overrides='{\"spec\":{\"nodeName\":\"$REMOTE\"}}' --image=curlimages/curl:8.10.1 --command -- \
sh -c 'curl -s --max-time 10 https://api.ipify.org; echo; sleep 3600'" >/dev/null
eventually 120 said_something || fail "member-first said nothing"
first=$(first_line)
[ "$first" = "$ADDR" ] || fail "member-first's first connection left as '$first', want $ADDR"
ok "1. a new member's first connection leaves as $ADDR"
for p in member-remote member-other member-holder; do
got=$(whoami_tcp $p || true)
[ "$got" = "$ADDR" ] || fail "A1: $p left as '$got', want $ADDR"
done
got=$(whoami_udp member-dns || true)
[ "$got" = "$ADDR" ] || fail "A1: member-dns (UDP) left as '$got', want $ADDR"
ok "A1/A2. members on $REMOTE, $OTHER and $HOLDER leave as $ADDR over TCP and UDP, replies come back"
for p in member-remote member-other member-holder; do
k -n egtest exec $p -- ping -c 2 -W 3 1.1.1.1 >/dev/null || fail "ping from $p got no reply"
done
ok "ping from members on all three nodes gets its replies"
# hop 1 is the member's own node, hop 2 the holder (it answers its own address, the member cannot see it); every
# later hop is a router on the internet whose time-exceeded comes back to ADDR and must find the member
hops=$(k -n egtest exec member-remote -- traceroute -n -m 8 -q 1 -w 2 1.1.1.1 2>&1 || true)
echo "$hops" | awk '$1 >= 3 && $2 ~ /^[0-9]+\./ {found=1} END {exit !found}' \
|| fail "no router beyond the holder answered the member's traceroute:
$hops"
ok "ICMP errors from routers beyond the holder reach a member on $REMOTE (traceroute)"
got=$(whoami_tcp plain || true)
[ "$got" = "$remote_ip" ] || fail "A4: non-member left as '$got', want its node $remote_ip"
plain_ip=$(k -n egtest get pod plain -o "jsonpath='{.status.podIP}'")
if $REMOTE_SSH "iptables-save | grep -F -- '$plain_ip/32'; ip rule | grep -F -- '$plain_ip'"; then
fail "A4: a rule names the non-member $plain_ip"
fi
ok "A4. the non-member leaves as its node ($remote_ip); no rule names it"
echo_ip=$(k -n egtest get pod echo -o "jsonpath='{.status.podIP}'")
member_ip=$(k -n egtest get pod member-remote -o "jsonpath='{.status.podIP}'")
got=$(k -n egtest exec member-remote -- curl -s --max-time 5 "http://$echo_ip:8080/clientip" || true)
[ "${got%:*}" = "$member_ip" ] || fail "A3: echo saw '$got', want the member's pod address $member_ip"
ok "A3. east-west: the echo pod sees the member's own address ($member_ip)"
k -n egtest patch svc mail --type merge -p "'{\"spec\":{\"loadBalancerIP\":\"192.0.2.1\"}}'" >/dev/null
eventually 60 status_is " " || fail "A5: status not cleared: '$(status)'"
got=$(whoami_tcp member-remote || true)
[ -z "$got" ] || fail "A5: with no holder a member still got out, as '$got'"
if k -n egtest exec member-remote -- ping -c 1 -W 3 1.1.1.1 >/dev/null 2>&1; then
fail "A5: with no holder a member's ping still got out"
fi
k -n egtest patch svc mail --type merge -p "'{\"spec\":{\"loadBalancerIP\":\"$ADDR\"}}'" >/dev/null
eventually 60 status_is "$want" || fail "status not restored: '$(status)'"
eventually 30 tcp_is member-remote "$ADDR" || fail "member did not get back out as $ADDR"
ok "A5. no holder: status cleared, members dropped (not leaked); restored afterwards"
k -n egtest delete egressservice mail >/dev/null
eventually 60 tcp_is member-remote "$remote_ip" || fail "A6: after delete, member left as '$(whoami_tcp member-remote || true)'"
if $REMOTE_SSH "ip rule | grep -E '^9100:'"; then fail "A6: member rules left on $REMOTE"; fi
if $HOLDER_SSH "ip rule | grep -E '^90:'"; then fail "A6: holder rules left on $HOLDER"; fi
ok "A6. EgressService deleted: members leave as their node again, no rules left"
echo PASS
+75
View File
@@ -0,0 +1,75 @@
# End-to-end test of weave-npc's LoadBalancer agent (lb). Placeholders are
# filled by run.sh: ADDR (an address HOLDER holds), HOLDER (that node),
# REMOTE (another node: its pod's replies must be steered back to HOLDER),
# OTHER (a third node: a pod there connects to ADDR from inside the cluster).
apiVersion: v1
kind: List
items:
- apiVersion: v1
kind: Namespace
metadata:
name: lbtest
# /clientip answers with the address the connection came from
- apiVersion: v1
kind: Pod
metadata:
name: echo-remote
namespace: lbtest
labels: {app: echo-remote}
spec:
nodeName: REMOTE
containers:
- name: echo
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports: [{name: http, containerPort: 8080}]
readinessProbe: {httpGet: {path: /healthz, port: 8080}}
- apiVersion: v1
kind: Pod
metadata:
name: echo-local
namespace: lbtest
labels: {app: echo-local}
spec:
nodeName: HOLDER
containers:
- name: echo
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["netexec", "--http-port=8080"]
ports: [{name: http, containerPort: 8080}]
readinessProbe: {httpGet: {path: /healthz, port: 8080}}
- apiVersion: v1
kind: Pod
metadata:
name: client
namespace: lbtest
spec:
nodeName: OTHER
containers:
- name: client
image: registry.k8s.io/e2e-test-images/agnhost:2.53
args: ["pause"]
- apiVersion: v1
kind: Service
metadata:
name: echo-remote
namespace: lbtest
spec:
type: LoadBalancer
loadBalancerClass: reweave.levk.dev/node-address
loadBalancerIP: ADDR
allocateLoadBalancerNodePorts: false
selector: {app: echo-remote}
ports: [{name: http, port: 18080, targetPort: http}]
- apiVersion: v1
kind: Service
metadata:
name: echo-local
namespace: lbtest
spec:
type: LoadBalancer
loadBalancerClass: reweave.levk.dev/node-address
loadBalancerIP: ADDR
allocateLoadBalancerNodePorts: false
selector: {app: echo-local}
ports: [{name: http, port: 18081, targetPort: http}]
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# End-to-end test of weave-npc's LoadBalancer agent against a live cluster.
#
# CP="ssh ... root@<control plane>" HOLDER_SSH="ssh ... root@<holder>" \
# ADDR=<address HOLDER holds> HOLDER=<node> REMOTE=<node> OTHER=<node> \
# reweave/test/lb/run.sh
#
# CP runs kubectl (admin.conf); HOLDER_SSH reads the holder's iptables. The
# machine running this script is the outside client. Asserts, in order:
# 1. each Service's status is exactly [{ip: ADDR, ipMode: Proxy}]
# 2. kube-proxy installed nothing for ADDR (ipMode Proxy leaves it to us)
# 3. from outside, both ports answer, and the pod sees the client's own
# address: from the holder's own pod, and from a pod on another node
# (whose replies must be steered back through the holder)
# 4. a pod on a third node reaches ADDR too
# 5. fail closed: naming an address nobody holds clears the status and
# stops the forward
# 6. deleting the Services removes the holder's chains
# Leaves nothing behind (namespace lbtest is deleted, also on failure).
set -euo pipefail
: "${CP:?}" "${HOLDER_SSH:?}" "${ADDR:?}" "${HOLDER:?}" "${REMOTE:?}" "${OTHER:?}"
here=$(cd "$(dirname "$0")" && pwd)
k() { $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl $*"; }
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo "ok: $*"; }
cleanup() { k delete namespace lbtest --ignore-not-found --wait=false >/dev/null 2>&1 || true; }
trap cleanup EXIT
# eventually <tries> <command...>: retry once a second
eventually() {
local n=$1; shift
for _ in $(seq "$n"); do
if "$@" >/dev/null 2>&1; then return 0; fi
sleep 1
done
return 1
}
sed -e "s/\bADDR\b/$ADDR/g; s/\bHOLDER\b/$HOLDER/g; s/\bREMOTE\b/$REMOTE/g; s/\bOTHER\b/$OTHER/g" \
"$here/lb-test.yaml" | $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f -" >/dev/null
k -n lbtest wait --for=condition=Ready pod --all --timeout=180s >/dev/null || fail "test pods not ready"
status() { k -n lbtest get svc "$1" -o "jsonpath='{.status.loadBalancer.ingress}'"; }
# each check re-reads on every try of eventually
status_is() { [ "$(status "$1")" = "$2" ]; }
chains_gone() { [ "$($HOLDER_SSH "iptables-save -t nat | grep -c '^:REWEAVE-LBS-'" || true)" = 0 ]; }
for s in echo-remote echo-local; do
want="[{\"ip\":\"$ADDR\",\"ipMode\":\"Proxy\"}]"
eventually 30 status_is $s "$want" || fail "$s status: $(status $s), want $want"
done
ok "1. status [{ip: $ADDR, ipMode: Proxy}] on both Services"
if $HOLDER_SSH "iptables-save -t nat | grep -E '^-A KUBE-' | grep -F -- '-d $ADDR/32'"; then
fail "kube-proxy has rules for $ADDR"
fi
ok "2. kube-proxy installed nothing for $ADDR"
# the client's own address, as the holder sees this machine's SSH session
me=$($HOLDER_SSH 'echo ${SSH_CLIENT%% *}')
for port in 18080 18081; do
got=""
for _ in $(seq 30); do
got=$(curl -s --max-time 3 "http://$ADDR:$port/clientip" || true)
[ -n "$got" ] && break
sleep 1
done
[ "${got%:*}" = "$me" ] || fail "port $port: pod saw '${got}', want this client ($me)"
done
ok "3. from outside, both pods see this client's own address ($me), remote pod's replies steered back"
k -n lbtest exec client -- /agnhost connect "$ADDR:18080" --timeout 5s >/dev/null || fail "pod on $OTHER cannot reach $ADDR:18080"
ok "4. a pod on $OTHER reaches $ADDR:18080"
k -n lbtest patch svc echo-remote --type merge -p "'{\"spec\":{\"loadBalancerIP\":\"192.0.2.1\"}}'" >/dev/null
eventually 30 status_is echo-remote "" || fail "status not cleared for an unheld address: $(status echo-remote)"
if curl -s --max-time 3 "http://$ADDR:18080/clientip" >/dev/null; then
fail "$ADDR:18080 still forwards after its Service moved to an unheld address"
fi
ok "5. unheld address: status cleared, forward gone (fail closed)"
k -n lbtest delete svc echo-remote echo-local >/dev/null
eventually 30 chains_gone || fail "holder kept per-port chains after the Services went away"
ok "6. Services deleted: holder's chains removed"
echo PASS
+14
View File
@@ -0,0 +1,14 @@
# netns.py <namespace> <pod>: the network namespace the weave CNI plugin attached a pod in, from the runtime's
# record of the attachment (libcni's result cache). Runs on the node.
import glob
import json
import sys
namespace, pod = sys.argv[1], sys.argv[2]
for path in glob.glob("/var/lib/cni/results/weave-*"):
with open(path) as f:
entry = json.load(f)
args = dict(entry.get("cniArgs") or [])
if args.get("K8S_POD_NAMESPACE") == namespace and args.get("K8S_POD_NAME") == pod:
print(entry["netns"])
break
+30
View File
@@ -0,0 +1,30 @@
# Pods for the port lock e2e (run.sh): a forger and a victim on node A, a victim on node B. None has any added
# capability; the forged frames are sent from the forger's network namespace by root on the node.
apiVersion: v1
kind: Namespace
metadata:
name: pltest
---
apiVersion: v1
kind: Pod
metadata: {name: forger, namespace: pltest}
spec:
nodeName: NODE_A
containers:
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"]}
---
apiVersion: v1
kind: Pod
metadata: {name: victim-local, namespace: pltest}
spec:
nodeName: NODE_A
containers:
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"]}
---
apiVersion: v1
kind: Pod
metadata: {name: victim-remote, namespace: pltest}
spec:
nodeName: NODE_B
containers:
- {name: c, image: "curlimages/curl:8.10.1", command: ["sleep", "3600"]}
+200
View File
@@ -0,0 +1,200 @@
// Command probe sends hand-made Ethernet frames out of an interface, or waits for one carrying a tag. The port lock
// e2e runs it inside pods' network namespaces (nsenter, as root on the node): a pod forging another's addresses
// must not reach anyone.
//
// probe send <ifname> <dst-mac> <src-mac|own> ipv4|arp <src-ip|own> <dst-ip> <tag>
// probe listen <ifname> <tag> <seconds>
package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"os"
"strconv"
"time"
"golang.org/x/sys/unix"
)
func main() {
if len(os.Args) < 2 {
usage()
}
var err error
switch os.Args[1] {
case "send":
if len(os.Args) != 9 {
usage()
}
err = send(os.Args[2], os.Args[3], os.Args[4], os.Args[5], os.Args[6], os.Args[7], os.Args[8])
case "listen":
if len(os.Args) != 5 {
usage()
}
secs, _ := strconv.Atoi(os.Args[4])
var seen bool
seen, err = listen(os.Args[2], os.Args[3], time.Duration(secs)*time.Second)
if err == nil {
if seen {
fmt.Println("seen")
return
}
fmt.Println("not seen")
os.Exit(1)
}
default:
usage()
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
}
func usage() {
fmt.Fprintln(os.Stderr, "usage: probe send <ifname> <dst-mac> <src-mac|own> ipv4|arp <src-ip|own> <dst-ip> <tag>")
fmt.Fprintln(os.Stderr, " probe listen <ifname> <tag> <seconds>")
os.Exit(2)
}
func send(ifName, dstMAC, srcMAC, kind, srcIP, dstIP, tag string) error {
iface, err := net.InterfaceByName(ifName)
if err != nil {
return err
}
dst, err := net.ParseMAC(dstMAC)
if err != nil {
return err
}
src := iface.HardwareAddr
if srcMAC != "own" {
if src, err = net.ParseMAC(srcMAC); err != nil {
return err
}
}
var from net.IP
if srcIP == "own" {
addrs, err := iface.Addrs()
if err != nil {
return err
}
for _, a := range addrs {
if n, ok := a.(*net.IPNet); ok && n.IP.To4() != nil {
from = n.IP.To4()
}
}
if from == nil {
return fmt.Errorf("%s has no IPv4 address", ifName)
}
} else if from = net.ParseIP(srcIP).To4(); from == nil {
return fmt.Errorf("bad source address %q", srcIP)
}
to := net.ParseIP(dstIP).To4()
if to == nil {
return fmt.Errorf("bad destination address %q", dstIP)
}
var frame []byte
switch kind {
case "ipv4":
frame = ipv4Frame(dst, src, from, to, []byte(tag))
case "arp":
frame = arpFrame(dst, src, from, to, []byte(tag))
default:
return fmt.Errorf("unknown kind %q", kind)
}
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL)))
if err != nil {
return err
}
defer unix.Close(fd)
if err := unix.Bind(fd, &unix.SockaddrLinklayer{Protocol: htons(unix.ETH_P_ALL), Ifindex: iface.Index}); err != nil {
return err
}
// a few copies, spaced out: the bridge and weave may still be learning where the destination is
for i := 0; i < 3; i++ {
if _, err := unix.Write(fd, frame); err != nil {
return err
}
time.Sleep(200 * time.Millisecond)
}
return nil
}
func listen(ifName, tag string, within time.Duration) (bool, error) {
iface, err := net.InterfaceByName(ifName)
if err != nil {
return false, err
}
fd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL)))
if err != nil {
return false, err
}
defer unix.Close(fd)
if err := unix.Bind(fd, &unix.SockaddrLinklayer{Protocol: htons(unix.ETH_P_ALL), Ifindex: iface.Index}); err != nil {
return false, err
}
tv := unix.NsecToTimeval((100 * time.Millisecond).Nanoseconds())
if err := unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil {
return false, err
}
buf := make([]byte, 65536)
deadline := time.Now().Add(within)
for time.Now().Before(deadline) {
n, _, err := unix.Recvfrom(fd, buf, 0)
if err != nil {
continue
}
if bytes.Contains(buf[:n], []byte(tag)) {
return true, nil
}
}
return false, nil
}
func htons(v uint16) uint16 { return v<<8 | v>>8 }
func ether(dst, src net.HardwareAddr, ethertype uint16, payload []byte) []byte {
f := make([]byte, 14, 14+len(payload))
copy(f[0:6], dst)
copy(f[6:12], src)
binary.BigEndian.PutUint16(f[12:14], ethertype)
return append(f, payload...)
}
func ipv4Frame(dst, src net.HardwareAddr, from, to net.IP, body []byte) []byte {
h := make([]byte, 20)
h[0] = 0x45
binary.BigEndian.PutUint16(h[2:4], uint16(20+len(body)))
h[8] = 64
h[9] = 253 // experimental protocol: nothing answers it, nothing on the way acts on it
copy(h[12:16], from)
copy(h[16:20], to)
binary.BigEndian.PutUint16(h[10:12], checksum(h))
return ether(dst, src, unix.ETH_P_IP, append(h, body...))
}
// arpFrame is an ARP request speaking for (src, from), asking for to; the tag rides as Ethernet trailer.
func arpFrame(dst, src net.HardwareAddr, from, to net.IP, tag []byte) []byte {
a := make([]byte, 28)
binary.BigEndian.PutUint16(a[0:2], 1)
binary.BigEndian.PutUint16(a[2:4], unix.ETH_P_IP)
a[4], a[5] = 6, 4
binary.BigEndian.PutUint16(a[6:8], 1)
copy(a[8:14], src)
copy(a[14:18], from)
copy(a[24:28], to)
return ether(dst, src, unix.ETH_P_ARP, append(a, tag...))
}
func checksum(b []byte) uint16 {
var s uint32
for i := 0; i+1 < len(b); i += 2 {
s += uint32(binary.BigEndian.Uint16(b[i:]))
}
for s>>16 != 0 {
s = s&0xffff + s>>16
}
return ^uint16(s)
}
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# End-to-end test of the port lock against a live cluster (assertion A7): a pod speaks only as itself.
#
# CP="ssh ... root@<cp>" A_SSH="ssh ... root@<a>" B_SSH="ssh ... root@<b>" A=<node a> B=<node b> \
# reweave/test/portlock/run.sh
#
# Asserts, in order:
# 0. every pod port on the three nodes (the CP's, A's and B's) carries the lock
# 1. a pod's own frames reach a pod on its node and a pod on another node (the control)
# 2. frames forging another pod's address reach neither
# 3. frames forging a node's bridge (its weave MAC and address) reach neither
# 4. ARP claiming another pod's address reaches no one; ARP for itself does
# The forged frames are sent from the forging pod's network namespace by root on its node (nsenter), the most a
# pod could ever do; the pod itself has no added capability.
# Leaves nothing behind (namespace pltest is deleted, also on failure; the probe is removed from the nodes).
set -euo pipefail
: "${CP:?}" "${A_SSH:?}" "${B_SSH:?}" "${A:?}" "${B:?}"
here=$(cd "$(dirname "$0")" && pwd)
k() { $CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl $*"; }
fail() { echo "FAIL: $*" >&2; exit 1; }
ok() { echo "ok: $*"; }
work=$(mktemp -d)
cleanup() {
k delete namespace pltest --ignore-not-found --wait=false >/dev/null 2>&1 || true
for s in "$A_SSH" "$B_SSH"; do $s 'rm -f /root/plprobe /root/plnetns.py' >/dev/null 2>&1 || true; done
rm -rf "$work"
}
trap cleanup EXIT
(cd "$here/../../.." && CGO_ENABLED=0 go build -o "$work/probe" ./reweave/test/portlock/probe/)
for s in "$A_SSH" "$B_SSH"; do
$s 'cat > /root/plprobe && chmod +x /root/plprobe' < "$work/probe"
$s 'cat > /root/plnetns.py' < "$here/netns.py"
done
# a port is locked when its ingress carries the three passes and the drop
unlocked() {
$1 'for p in $(ip -br link show master weave | sed "s/@.*//; s/ .*//" | grep ^vethwepl); do
f=$(tc filter show dev $p ingress)
for want in "protocol ip pref 1 u32" "protocol arp pref 2 u32" "protocol arp pref 3 u32" \
"protocol all pref 9 matchall" "gact action drop"; do
echo "$f" | grep -q "$want" || { echo "$p"; break; }
done
done'
}
all_locked() {
for s in "$CP" "$A_SSH" "$B_SSH"; do
u=$(unlocked "$s")
[ -z "$u" ] || fail "0. unlocked pod ports ($1): $u"
done
}
all_locked "before the test pods"
ok "0. every pod port on the three nodes carries the lock"
sed -e "s/\bNODE_A\b/$A/g; s/\bNODE_B\b/$B/g" "$here/portlock-test.yaml" |
$CP "KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f -" >/dev/null
k -n pltest wait --for=condition=Ready pod --all --timeout=300s >/dev/null || fail "test pods not ready"
all_locked "with the test pods"
netns_of() { $1 "python3 /root/plnetns.py pltest $2"; }
mac_in() { $1 "nsenter --net=$2 ip -br link show eth0" | awk '{print $3}'; }
pod_ip() { k -n pltest get pod "$1" -o "jsonpath='{.status.podIP}'"; }
f_ns=$(netns_of "$A_SSH" forger)
vl_ns=$(netns_of "$A_SSH" victim-local)
vr_ns=$(netns_of "$B_SSH" victim-remote)
[ -n "$f_ns" ] && [ -n "$vl_ns" ] && [ -n "$vr_ns" ] || fail "test pods' namespaces not found"
vl_mac=$(mac_in "$A_SSH" "$vl_ns")
vr_mac=$(mac_in "$B_SSH" "$vr_ns")
vl_ip=$(pod_ip victim-local)
vr_ip=$(pod_ip victim-remote)
# the nodes' bridges, from the annotation the lb agent writes ("ip,mac")
bridge_of() { k get node "$1" -o "jsonpath='{.metadata.annotations.reweave\.levk\.dev/weave}'"; }
b_bridge=$(bridge_of "$B"); b_ip=${b_bridge%,*}; b_mac=${b_bridge#*,}
a_bridge=$(bridge_of "$A"); a_ip=${a_bridge%,*}; a_mac=${a_bridge#*,}
[ -n "$b_ip" ] && [ -n "$b_mac" ] && [ -n "$a_ip" ] && [ -n "$a_mac" ] || fail "no weave bridge annotation on $A or $B"
# expect <seen|not seen> <victim: local|remote> <tag> <send args...>: the victim listens while the forger sends
expect() {
local want=$1 where=$2 tag=$3-$RANDOM
shift 3
local vs vns
if [ "$where" = local ]; then vs=$A_SSH vns=$vl_ns; else vs=$B_SSH vns=$vr_ns; fi
$vs "nsenter --net=$vns /root/plprobe listen eth0 $tag 6" > "$work/listen" 2>&1 &
local listener=$!
sleep 1
$A_SSH "nsenter --net=$f_ns /root/plprobe send eth0 $* $tag" || fail "probe could not send: $*"
wait $listener || true
local got
got=$(cat "$work/listen")
[ "$got" = "$want" ] || fail "$tag ($where): '$got', want '$want'"
}
expect "seen" local own-local "$vl_mac own ipv4 own $vl_ip"
expect "seen" remote own-remote "$vr_mac own ipv4 own $vr_ip"
ok "1. the forger's own frames reach a pod on its node and one on $B"
expect "not seen" remote pod-ip-remote "$vr_mac own ipv4 $vl_ip $vr_ip"
expect "not seen" local pod-ip-local "$vl_mac own ipv4 $vr_ip $vl_ip"
expect "not seen" remote pod-mac-remote "$vr_mac $vl_mac ipv4 $vl_ip $vr_ip"
ok "2. frames forging another pod's address reach no one"
expect "not seen" local node-b-local "$vl_mac $b_mac ipv4 $b_ip $vl_ip"
expect "not seen" remote node-b-remote "$vr_mac $b_mac ipv4 $b_ip $vr_ip"
expect "not seen" remote node-a-remote "$vr_mac $a_mac ipv4 $a_ip $vr_ip"
ok "3. frames forging a node's bridge ($A's, $B's) reach no one"
expect "not seen" local arp-claim-local "ff:ff:ff:ff:ff:ff own arp $vr_ip $vl_ip"
expect "not seen" remote arp-claim-remote "ff:ff:ff:ff:ff:ff own arp $vl_ip $vr_ip"
expect "seen" remote arp-own-remote "ff:ff:ff:ff:ff:ff own arp own $vr_ip"
ok "4. ARP claiming another pod's address reaches no one; the forger's ARP for itself does"
echo "PASS"
+30
View File
@@ -0,0 +1,30 @@
linters:
enable:
- exportloopref # Checks for pointers to enclosing loop variables
- gofmt
- goimports
- gosec
- ineffassign
- misspell
- nolintlint
- revive
- staticcheck
- tenv # Detects using os.Setenv instead of t.Setenv since Go 1.17
- unconvert
- unused
- vet
- dupword # Checks for duplicate words in the source code
disable:
- errcheck
run:
timeout: 5m
skip-dirs:
- api
- cluster
- design
- docs
- docs/man
- releases
- reports
- test # e2e scripts
@@ -1,7 +1,7 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -176,13 +176,13 @@
END OF TERMS AND CONDITIONS
Copyright 2014 Docker, Inc.
Copyright The containerd Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
+17
View File
@@ -0,0 +1,17 @@
# log
A Go package providing a common logging interface across containerd repositories and a way for clients to use and configure logging in containerd packages.
This package is not intended to be used as a standalone logging package outside of the containerd ecosystem and is intended as an interface wrapper around a logging implementation.
In the future this package may be replaced with a common go logging interface.
## Project details
**log** is a containerd sub-project, licensed under the [Apache 2.0 license](./LICENSE).
As a containerd sub-project, you will find the:
* [Project governance](https://github.com/containerd/project/blob/main/GOVERNANCE.md),
* [Maintainers](https://github.com/containerd/project/blob/main/MAINTAINERS),
* and [Contributing guidelines](https://github.com/containerd/project/blob/main/CONTRIBUTING.md)
information in our [`containerd/project`](https://github.com/containerd/project) repository.
+182
View File
@@ -0,0 +1,182 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package log provides types and functions related to logging, passing
// loggers through a context, and attaching context to the logger.
//
// # Transitional types
//
// This package contains various types that are aliases for types in [logrus].
// These aliases are intended for transitioning away from hard-coding logrus
// as logging implementation. Consumers of this package are encouraged to use
// the type-aliases from this package instead of directly using their logrus
// equivalent.
//
// The intent is to replace these aliases with locally defined types and
// interfaces once all consumers are no longer directly importing logrus
// types.
//
// IMPORTANT: due to the transitional purpose of this package, it is not
// guaranteed for the full logrus API to be provided in the future. As
// outlined, these aliases are provided as a step to transition away from
// a specific implementation which, as a result, exposes the full logrus API.
// While no decisions have been made on the ultimate design and interface
// provided by this package, we do not expect carrying "less common" features.
package log
import (
"context"
"fmt"
"github.com/sirupsen/logrus"
)
// G is a shorthand for [GetLogger].
//
// We may want to define this locally to a package to get package tagged log
// messages.
var G = GetLogger
// L is an alias for the standard logger.
var L = &Entry{
Logger: logrus.StandardLogger(),
// Default is three fields plus a little extra room.
Data: make(Fields, 6),
}
type loggerKey struct{}
// Fields type to pass to "WithFields".
type Fields = map[string]any
// Entry is a logging entry. It contains all the fields passed with
// [Entry.WithFields]. It's finally logged when Trace, Debug, Info, Warn,
// Error, Fatal or Panic is called on it. These objects can be reused and
// passed around as much as you wish to avoid field duplication.
//
// Entry is a transitional type, and currently an alias for [logrus.Entry].
type Entry = logrus.Entry
// RFC3339NanoFixed is [time.RFC3339Nano] with nanoseconds padded using
// zeros to ensure the formatted time is always the same number of
// characters.
const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
// Level is a logging level.
type Level = logrus.Level
// Supported log levels.
const (
// TraceLevel level. Designates finer-grained informational events
// than [DebugLevel].
TraceLevel Level = logrus.TraceLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose
// logging.
DebugLevel Level = logrus.DebugLevel
// InfoLevel level. General operational entries about what's going on
// inside the application.
InfoLevel Level = logrus.InfoLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel Level = logrus.WarnLevel
// ErrorLevel level. Logs errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel Level = logrus.ErrorLevel
// FatalLevel level. Logs and then calls "logger.Exit(1)". It exits
// even if the logging level is set to Panic.
FatalLevel Level = logrus.FatalLevel
// PanicLevel level. This is the highest level of severity. Logs and
// then calls panic with the message passed to Debug, Info, ...
PanicLevel Level = logrus.PanicLevel
)
// SetLevel sets log level globally. It returns an error if the given
// level is not supported.
//
// level can be one of:
//
// - "trace" ([TraceLevel])
// - "debug" ([DebugLevel])
// - "info" ([InfoLevel])
// - "warn" ([WarnLevel])
// - "error" ([ErrorLevel])
// - "fatal" ([FatalLevel])
// - "panic" ([PanicLevel])
func SetLevel(level string) error {
lvl, err := logrus.ParseLevel(level)
if err != nil {
return err
}
L.Logger.SetLevel(lvl)
return nil
}
// GetLevel returns the current log level.
func GetLevel() Level {
return L.Logger.GetLevel()
}
// OutputFormat specifies a log output format.
type OutputFormat string
// Supported log output formats.
const (
// TextFormat represents the text logging format.
TextFormat OutputFormat = "text"
// JSONFormat represents the JSON logging format.
JSONFormat OutputFormat = "json"
)
// SetFormat sets the log output format ([TextFormat] or [JSONFormat]).
func SetFormat(format OutputFormat) error {
switch format {
case TextFormat:
L.Logger.SetFormatter(&logrus.TextFormatter{
TimestampFormat: RFC3339NanoFixed,
FullTimestamp: true,
})
return nil
case JSONFormat:
L.Logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: RFC3339NanoFixed,
})
return nil
default:
return fmt.Errorf("unknown log format: %s", format)
}
}
// WithLogger returns a new context with the provided logger. Use in
// combination with logger.WithField(s) for great effect.
func WithLogger(ctx context.Context, logger *Entry) context.Context {
return context.WithValue(ctx, loggerKey{}, logger.WithContext(ctx))
}
// GetLogger retrieves the current logger from the context. If no logger is
// available, the default logger is returned.
func GetLogger(ctx context.Context) *Entry {
if logger := ctx.Value(loggerKey{}); logger != nil {
return logger.(*Entry)
}
return L.WithContext(ctx)
}
+1
View File
@@ -0,0 +1 @@
*.go text eol=lf
+2
View File
@@ -0,0 +1,2 @@
# Cover profiles
*.out
+18
View File
@@ -0,0 +1,18 @@
linters:
enable:
- bodyclose
- dupword # Checks for duplicate words in the source code
- gofmt
- goimports
- ineffassign
- misspell
- revive
- staticcheck
- unconvert
- unused
- vet
disable:
- errcheck
run:
deadline: 2m
+5
View File
@@ -0,0 +1,5 @@
# Code of Conduct
We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).
Please contact the [CNCF Code of Conduct Committee](mailto:conduct@cncf.io) in order to report violations of the Code of Conduct.
+114
View File
@@ -0,0 +1,114 @@
# Contributing to the reference library
## Community help
If you need help, please ask in the [#distribution](https://cloud-native.slack.com/archives/C01GVR8SY4R) channel on CNCF community slack.
[Click here for an invite to the CNCF community slack](https://slack.cncf.io/)
## Reporting security issues
The maintainers take security seriously. If you discover a security
issue, please bring it to their attention right away!
Please **DO NOT** file a public issue, instead send your report privately to
[cncf-distribution-security@lists.cncf.io](mailto:cncf-distribution-security@lists.cncf.io).
## Reporting an issue properly
By following these simple rules you will get better and faster feedback on your issue.
- search the bugtracker for an already reported issue
### If you found an issue that describes your problem:
- please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments
- please refrain from adding "same thing here" or "+1" comments
- you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button
- comment if you have some new, technical and relevant information to add to the case
- __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue.
### If you have not found an existing issue that describes your problem:
1. create a new issue, with a succinct title that describes your issue:
- bad title: "It doesn't work with my docker"
- good title: "Private registry push fail: 400 error with E_INVALID_DIGEST"
2. copy the output of (or similar for other container tools):
- `docker version`
- `docker info`
- `docker exec <registry-container> registry --version`
3. copy the command line you used to launch your Registry
4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments)
5. reproduce your problem and get your docker daemon logs showing the error
6. if relevant, copy your registry logs that show the error
7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used)
8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry
## Contributing Code
Contributions should be made via pull requests. Pull requests will be reviewed
by one or more maintainers or reviewers and merged when acceptable.
You should follow the basic GitHub workflow:
1. Use your own [fork](https://help.github.com/en/articles/about-forks)
2. Create your [change](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
3. Test your code
4. [Commit](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages) your work, always [sign your commits](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#commit-messages)
5. Push your change to your fork and create a [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork)
Refer to [containerd's contribution guide](https://github.com/containerd/project/blob/master/CONTRIBUTING.md#successful-changes)
for tips on creating a successful contribution.
## Sign your work
The sign-off is a simple line at the end of the explanation for the patch. Your
signature certifies that you wrote the patch or otherwise have the right to pass
it on as an open-source patch. The rules are pretty simple: if you can certify
the below (from [developercertificate.org](http://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
+144
View File
@@ -0,0 +1,144 @@
# distribution/reference Project Governance
Distribution [Code of Conduct](./CODE-OF-CONDUCT.md) can be found here.
For specific guidance on practical contribution steps please
see our [CONTRIBUTING.md](./CONTRIBUTING.md) guide.
## Maintainership
There are different types of maintainers, with different responsibilities, but
all maintainers have 3 things in common:
1) They share responsibility in the project's success.
2) They have made a long-term, recurring time investment to improve the project.
3) They spend that time doing whatever needs to be done, not necessarily what
is the most interesting or fun.
Maintainers are often under-appreciated, because their work is harder to appreciate.
It's easy to appreciate a really cool and technically advanced feature. It's harder
to appreciate the absence of bugs, the slow but steady improvement in stability,
or the reliability of a release process. But those things distinguish a good
project from a great one.
## Reviewers
A reviewer is a core role within the project.
They share in reviewing issues and pull requests and their LGTM counts towards the
required LGTM count to merge a code change into the project.
Reviewers are part of the organization but do not have write access.
Becoming a reviewer is a core aspect in the journey to becoming a maintainer.
## Adding maintainers
Maintainers are first and foremost contributors that have shown they are
committed to the long term success of a project. Contributors wanting to become
maintainers are expected to be deeply involved in contributing code, pull
request review, and triage of issues in the project for more than three months.
Just contributing does not make you a maintainer, it is about building trust
with the current maintainers of the project and being a person that they can
depend on and trust to make decisions in the best interest of the project.
Periodically, the existing maintainers curate a list of contributors that have
shown regular activity on the project over the prior months. From this list,
maintainer candidates are selected and proposed in a pull request or a
maintainers communication channel.
After a candidate has been announced to the maintainers, the existing
maintainers are given five business days to discuss the candidate, raise
objections and cast their vote. Votes may take place on the communication
channel or via pull request comment. Candidates must be approved by at least 66%
of the current maintainers by adding their vote on the mailing list. The
reviewer role has the same process but only requires 33% of current maintainers.
Only maintainers of the repository that the candidate is proposed for are
allowed to vote.
If a candidate is approved, a maintainer will contact the candidate to invite
the candidate to open a pull request that adds the contributor to the
MAINTAINERS file. The voting process may take place inside a pull request if a
maintainer has already discussed the candidacy with the candidate and a
maintainer is willing to be a sponsor by opening the pull request. The candidate
becomes a maintainer once the pull request is merged.
## Stepping down policy
Life priorities, interests, and passions can change. If you're a maintainer but
feel you must remove yourself from the list, inform other maintainers that you
intend to step down, and if possible, help find someone to pick up your work.
At the very least, ensure your work can be continued where you left off.
After you've informed other maintainers, create a pull request to remove
yourself from the MAINTAINERS file.
## Removal of inactive maintainers
Similar to the procedure for adding new maintainers, existing maintainers can
be removed from the list if they do not show significant activity on the
project. Periodically, the maintainers review the list of maintainers and their
activity over the last three months.
If a maintainer has shown insufficient activity over this period, a neutral
person will contact the maintainer to ask if they want to continue being
a maintainer. If the maintainer decides to step down as a maintainer, they
open a pull request to be removed from the MAINTAINERS file.
If the maintainer wants to remain a maintainer, but is unable to perform the
required duties they can be removed with a vote of at least 66% of the current
maintainers. In this case, maintainers should first propose the change to
maintainers via the maintainers communication channel, then open a pull request
for voting. The voting period is five business days. The voting pull request
should not come as a surpise to any maintainer and any discussion related to
performance must not be discussed on the pull request.
## How are decisions made?
Docker distribution is an open-source project with an open design philosophy.
This means that the repository is the source of truth for EVERY aspect of the
project, including its philosophy, design, road map, and APIs. *If it's part of
the project, it's in the repo. If it's in the repo, it's part of the project.*
As a result, all decisions can be expressed as changes to the repository. An
implementation change is a change to the source code. An API change is a change
to the API specification. A philosophy change is a change to the philosophy
manifesto, and so on.
All decisions affecting distribution, big and small, follow the same 3 steps:
* Step 1: Open a pull request. Anyone can do this.
* Step 2: Discuss the pull request. Anyone can do this.
* Step 3: Merge or refuse the pull request. Who does this depends on the nature
of the pull request and which areas of the project it affects.
## Helping contributors with the DCO
The [DCO or `Sign your work`](./CONTRIBUTING.md#sign-your-work)
requirement is not intended as a roadblock or speed bump.
Some contributors are not as familiar with `git`, or have used a web
based editor, and thus asking them to `git commit --amend -s` is not the best
way forward.
In this case, maintainers can update the commits based on clause (c) of the DCO.
The most trivial way for a contributor to allow the maintainer to do this, is to
add a DCO signature in a pull requests's comment, or a maintainer can simply
note that the change is sufficiently trivial that it does not substantially
change the existing contribution - i.e., a spelling change.
When you add someone's DCO, please also add your own to keep a log.
## I'm a maintainer. Should I make pull requests too?
Yes. Nobody should ever push to master directly. All changes should be
made through a pull request.
## Conflict Resolution
If you have a technical dispute that you feel has reached an impasse with a
subset of the community, any contributor may open an issue, specifically
calling for a resolution vote of the current core maintainers to resolve the
dispute. The same voting quorums required (2/3) for adding and removing
maintainers will apply to conflict resolution.
File renamed without changes.
+26
View File
@@ -0,0 +1,26 @@
# Distribution project maintainers & reviewers
#
# See GOVERNANCE.md for maintainer versus reviewer roles
#
# MAINTAINERS (cncf-distribution-maintainers@lists.cncf.io)
# GitHub ID, Name, Email address
"chrispat","Chris Patterson","chrispat@github.com"
"clarkbw","Bryan Clark","clarkbw@github.com"
"corhere","Cory Snider","csnider@mirantis.com"
"deleteriousEffect","Hayley Swimelar","hswimelar@gitlab.com"
"heww","He Weiwei","hweiwei@vmware.com"
"joaodrp","João Pereira","jpereira@gitlab.com"
"justincormack","Justin Cormack","justin.cormack@docker.com"
"squizzi","Kyle Squizzato","ksquizzato@mirantis.com"
"milosgajdos","Milos Gajdos","milosthegajdos@gmail.com"
"sargun","Sargun Dhillon","sargun@sargun.me"
"wy65701436","Wang Yan","wangyan@vmware.com"
"stevelasker","Steve Lasker","steve.lasker@microsoft.com"
#
# REVIEWERS
# GitHub ID, Name, Email address
"dmcgowan","Derek McGowan","derek@mcgstyle.net"
"stevvooe","Stephen Day","stevvooe@gmail.com"
"thajeztah","Sebastiaan van Stijn","github@gone.nl"
"DavidSpek", "David van der Spek", "vanderspek.david@gmail.com"
"Jamstah", "James Hewitt", "james.hewitt@gmail.com"
+25
View File
@@ -0,0 +1,25 @@
# Project packages.
PACKAGES=$(shell go list ./...)
# Flags passed to `go test`
BUILDFLAGS ?=
TESTFLAGS ?=
.PHONY: all build test coverage
.DEFAULT: all
all: build
build: ## no binaries to build, so just check compilation suceeds
go build ${BUILDFLAGS} ./...
test: ## run tests
go test ${TESTFLAGS} ./...
coverage: ## generate coverprofiles from the unit tests
rm -f coverage.txt
go test ${TESTFLAGS} -cover -coverprofile=cover.out ./...
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+30
View File
@@ -0,0 +1,30 @@
# Distribution reference
Go library to handle references to container images.
<img src="/distribution-logo.svg" width="200px" />
[![Build Status](https://github.com/distribution/reference/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/distribution/reference/actions?query=workflow%3ACI)
[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/reference)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
[![codecov](https://codecov.io/gh/distribution/reference/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/reference)
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Fdistribution%2Freference?ref=badge_shield)
This repository contains a library for handling references to container images held in container registries. Please see [godoc](https://pkg.go.dev/github.com/distribution/reference) for details.
## Contribution
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
issues, fixes, and patches to this project.
## Communication
For async communication and long running discussions please use issues and pull requests on the github repo.
This will be the best place to discuss design and implementation.
For sync communication we have a #distribution channel in the [CNCF Slack](https://slack.cncf.io/)
that everyone is welcome to join and chat about development.
## Licenses
The distribution codebase is released under the [Apache 2.0 license](LICENSE).
+7
View File
@@ -0,0 +1,7 @@
# Security Policy
## Reporting a Vulnerability
The maintainers take security seriously. If you discover a security issue, please bring it to their attention right away!
Please DO NOT file a public issue, instead send your report privately to cncf-distribution-security@lists.cncf.io.
+1
View File
@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564.27793 654.82002"><defs><style>.cls-1{fill:#416ba9;}.cls-2{fill:#74c3d5;}</style></defs><path class="cls-1" d="M17.48582,567.55941c0-3.66375,2.2168-4.91716,4.91609-4.91716H36.66877c18.70153,0,28.24476,9.06127,28.24476,33.64387,0,22.26771-7.51889,35.57065-27.95526,35.57065H22.40191c-2.69929,0-4.91609-1.25341-4.91609-4.91609Zm20.33987,49.741c7.61539,0,10.60365-6.94043,10.60365-20.72586,0-15.13429-3.85568-19.376-10.70015-19.376H33.97v40.10182Z"/><path class="cls-1" d="M72.43613,567.55941c0-3.66375,2.21681-4.91716,4.9161-4.91716h6.36251c2.69876,0,4.91609,1.25341,4.91609,4.91716v59.38127c0,3.66268-2.21733,4.91609-4.91609,4.91609H77.35223c-2.69929,0-4.9161-1.25341-4.9161-4.91609Z"/><path class="cls-1" d="M99.718,613.05875l5.78405-1.06042c4.33764-.77092,4.53063,1.15692,5.78352,3.85674a6.70953,6.70953,0,0,0,6.5555,3.95218c3.56672,0,6.26548-1.83134,6.26548-5.59158,0-12.82046-27.6663-11.08561-27.6663-34.31724,0-10.79718,7.80839-18.1236,20.43637-18.1236,11.08614,0,16.96615,4.8196,20.05091,12.918.96445,2.31277,1.44641,4.7231-2.98826,5.59052l-5.20559,1.06042c-3.95218.77092-4.33764-1.06042-6.073-3.66269a6.39679,6.39679,0,0,0-5.49456-3.08475c-3.18125,0-5.20559,1.83134-5.20559,4.7231,0,12.24252,27.56981,10.60418,27.56981,33.73931,0,12.532-8.77231,19.66545-22.36422,19.66545-11.47159,0-17.93007-4.43467-20.82236-14.07387C95.57286,616.24,95.1874,613.92617,99.718,613.05875Z"/><path class="cls-1" d="M154.86187,576.62068h-8.09735c-3.66321,0-4.91662-2.21734-4.91662-4.91716v-4.14411c0-2.69983,1.25341-4.91716,4.91662-4.91716H179.058c3.66321,0,4.91609,2.21733,4.91609,4.91716v4.14411c0,2.69982-1.25288,4.91716-4.91609,4.91716h-8.09788v50.32c0,3.66268-2.21681,4.91609-4.9161,4.91609h-6.266c-2.69876,0-4.9161-1.25341-4.9161-4.91609Z"/><path class="cls-1" d="M189.76022,567.55941c0-3.66375,2.21681-4.91716,4.9161-4.91716h16.2912c15.13481,0,23.03969,5.68808,23.03969,20.05144,0,8.38684-3.27775,13.68787-8.29034,16.77369l9.35076,27.76173c1.34938,4.04974-.96445,4.62766-3.37425,4.62766h-8.67581c-2.89176,0-4.53063-1.92783-5.30208-4.7231L211.546,605.92532h-5.68755v21.01536c0,3.66268-2.2168,4.91609-4.91609,4.91609h-6.266c-2.69929,0-4.9161-1.25341-4.9161-4.91609Zm20.14688,25.35246c4.14517,0,7.61592-1.6394,7.61592-8.57984,0-7.61486-3.37425-8.19384-7.22993-8.19384h-4.43467v16.77368Z"/><path class="cls-1" d="M241.91422,567.55941c0-3.66375,2.2168-4.91716,4.91609-4.91716h6.36251c2.69876,0,4.9161,1.25341,4.9161,4.91716v59.38127c0,3.66268-2.21734,4.91609-4.9161,4.91609h-6.36251c-2.69929,0-4.91609-1.25341-4.91609-4.91609Z"/><path class="cls-1" d="M267.75024,567.55941c0-3.66375,2.2168-4.91716,4.91609-4.91716h14.94182c16.77316,0,24.09958,5.49508,24.09958,18.31659,0,6.94044-3.08475,11.953-8.96477,14.5553v.193c6.74744,2.31384,10.41065,7.13343,10.41065,15.80977,0,16.58069-11.3751,20.33987-25.449,20.33987H272.66633c-2.69929,0-4.91609-1.25341-4.91609-4.91609Zm20.72533,22.5572c4.33817,0,6.84447-2.12083,6.84447-7.32642,0-4.91716-2.79579-6.36251-6.84447-6.36251h-4.9161v13.68893Zm.386,27.95473c5.88,0,7.90436-2.40926,7.90436-7.80785,0-5.88-3.18126-7.80786-7.80839-7.80786h-5.39806v15.61571Z"/><path class="cls-1" d="M320.09776,567.55941c0-3.66375,2.21733-4.91716,4.91662-4.91716h6.362c2.69929,0,4.91663,1.25341,4.91663,4.91716v43.282c0,5.78457,2.3133,8.38684,6.748,8.38684,4.43413,0,7.13343-2.60227,7.13343-8.38684v-43.282c0-3.66375,2.21733-4.91716,4.91609-4.91716H361.26c2.6993,0,4.91663,1.25341,4.91663,4.91716v41.06573c0,15.80871-7.13343,24.00256-23.42516,24.00256-16.0982,0-22.6537-8.19385-22.6537-24.00256Z"/><path class="cls-1" d="M384.59236,576.62068H376.495c-3.66322,0-4.91663-2.21734-4.91663-4.91716v-4.14411c0-2.69983,1.25341-4.91716,4.91663-4.91716h32.29343c3.66321,0,4.91609,2.21733,4.91609,4.91716v4.14411c0,2.69982-1.25288,4.91716-4.91609,4.91716H400.6911v50.32c0,3.66268-2.21734,4.91609-4.91663,4.91609h-6.266c-2.69929,0-4.9161-1.25341-4.9161-4.91609Z"/><path class="cls-1" d="M419.49071,567.55941c0-3.66375,2.21681-4.91716,4.9161-4.91716h6.36251c2.69876,0,4.91609,1.25341,4.91609,4.91716v59.38127c0,3.66268-2.21733,4.91609-4.91609,4.91609h-6.36251c-2.69929,0-4.9161-1.25341-4.9161-4.91609Z"/><path class="cls-1" d="M443.20536,597.34654c0-21.30485,5.59106-35.57171,24.48505-35.57171s24.48505,14.26686,24.48505,35.57171c0,22.07472-6.84394,35.37766-24.485,35.37766S443.20536,619.42126,443.20536,597.34654Zm32.48643,0c0-16.29226-2.12083-22.26877-8.00138-22.26877-5.88,0-8.00085,5.97651-8.00085,22.26877,0,17.06212,2.4098,22.17121,8.00085,22.17121C473.378,619.51775,475.69179,614.40866,475.69179,597.34654Z"/><path class="cls-1" d="M499.69859,567.55941c0-3.66375,2.2168-4.91716,4.9161-4.91716h6.941a7.56728,7.56728,0,0,1,6.84394,4.43467l13.207,29.88363h.19247V567.55941c0-3.66375,2.21733-4.91716,4.91662-4.91716h4.62714c2.69876,0,4.91609,1.25341,4.91609,4.91716v59.38127c0,3.66268-2.21733,4.91609-4.91609,4.91609h-5.68756c-2.98878,0-4.72363-1.83134-5.977-4.43466L514.15792,593.5863h-.19247v33.35438c0,3.66268-Line truncated

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -32,7 +32,7 @@ func FamiliarString(ref Reference) string {
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
// See [path.Match] for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
+255
View File
@@ -0,0 +1,255 @@
package reference
import (
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
const (
// legacyDefaultDomain is the legacy domain for Docker Hub (which was
// originally named "the Docker Index"). This domain is still used for
// authentication and image search, which were part of the "v1" Docker
// registry specification.
//
// This domain will continue to be supported, but there are plans to consolidate
// legacy domains to new "canonical" domains. Once those domains are decided
// on, we must update the normalization functions, but preserve compatibility
// with existing installs, clients, and user configuration.
legacyDefaultDomain = "index.docker.io"
// defaultDomain is the default domain used for images on Docker Hub.
// It is used to normalize "familiar" names to canonical names, for example,
// to convert "ubuntu" to "docker.io/library/ubuntu:latest".
//
// Note that actual domain of Docker Hub's registry is registry-1.docker.io.
// This domain will continue to be supported, but there are plans to consolidate
// legacy domains to new "canonical" domains. Once those domains are decided
// on, we must update the normalization functions, but preserve compatibility
// with existing installs, clients, and user configuration.
defaultDomain = "docker.io"
// officialRepoPrefix is the namespace used for official images on Docker Hub.
// It is used to normalize "familiar" names to canonical names, for example,
// to convert "ubuntu" to "docker.io/library/ubuntu:latest".
officialRepoPrefix = "library/"
// defaultTag is the default tag if no tag is provided.
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remote string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remote = remainder[:tagSep]
} else {
remote = remainder
}
if strings.ToLower(remote) != remote {
return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remote)
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// namedTaggedDigested is a reference that has both a tag and a digest.
type namedTaggedDigested interface {
NamedTagged
Digested
}
// ParseDockerRef normalizes the image reference following the docker convention,
// which allows for references to contain both a tag and a digest. It returns a
// reference that is either tagged or digested. For references containing both
// a tag and a digest, it returns a digested reference. For example, the following
// reference:
//
// docker.io/library/busybox:latest@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
//
// Is returned as a digested reference (with the ":latest" tag removed):
//
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
//
// References that are already "tagged" or "digested" are returned unmodified:
//
// // Already a digested reference
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa
//
// // Already a named reference
// docker.io/library/busybox:latest
func ParseDockerRef(ref string) (Named, error) {
named, err := ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if canonical, ok := named.(namedTaggedDigested); ok {
// The reference is both tagged and digested; only return digested.
newNamed, err := WithName(canonical.Name())
if err != nil {
return nil, err
}
return WithDigest(newNamed, canonical.Digest())
}
return TagNameOnly(named), nil
}
// splitDockerDomain splits a repository name to domain and remote-name.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remoteName string) {
maybeDomain, maybeRemoteName, ok := strings.Cut(name, "/")
if !ok {
// Fast-path for single element ("familiar" names), such as "ubuntu"
// or "ubuntu:latest". Familiar names must be handled separately, to
// prevent them from being handled as "hostname:port".
//
// Canonicalize them as "docker.io/library/name[:tag]"
// FIXME(thaJeztah): account for bare "localhost" or "example.com" names, which SHOULD be considered a domain.
return defaultDomain, officialRepoPrefix + name
}
switch {
case maybeDomain == localhost:
// localhost is a reserved namespace and always considered a domain.
domain, remoteName = maybeDomain, maybeRemoteName
case maybeDomain == legacyDefaultDomain:
// canonicalize the Docker Hub and legacy "Docker Index" domains.
domain, remoteName = defaultDomain, maybeRemoteName
case strings.ContainsAny(maybeDomain, ".:"):
// Likely a domain or IP-address:
//
// - contains a "." (e.g., "example.com" or "127.0.0.1")
// - contains a ":" (e.g., "example:5000", "::1", or "[::1]:5000")
domain, remoteName = maybeDomain, maybeRemoteName
case strings.ToLower(maybeDomain) != maybeDomain:
// Uppercase namespaces are not allowed, so if the first element
// is not lowercase, we assume it to be a domain-name.
domain, remoteName = maybeDomain, maybeRemoteName
default:
// None of the above: it's not a domain, so use the default, and
// use the name input the remote-name.
domain, remoteName = defaultDomain, name
}
if domain == defaultDomain && !strings.ContainsRune(remoteName, '/') {
// Canonicalize "familiar" names, but only on Docker Hub, not
// on other domains:
//
// "docker.io/ubuntu[:tag]" => "docker.io/library/ubuntu[:tag]"
remoteName = officialRepoPrefix + remoteName
}
return domain, remoteName
}
// familiarizeName returns a shortened version of the name familiar
// to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if strings.HasPrefix(repo.path, officialRepoPrefix) {
// TODO(thaJeztah): this check may be too strict, as it assumes the
// "library/" namespace does not have nested namespaces. While this
// is true (currently), technically it would be possible for Docker
// Hub to use those (e.g. "library/distros/ubuntu:latest").
// See https://github.com/distribution/distribution/pull/3769#issuecomment-1302031785.
if remainder := strings.TrimPrefix(repo.path, officialRepoPrefix); !strings.ContainsRune(remainder, '/') {
repo.path = remainder
}
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}
@@ -4,11 +4,14 @@
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [domain '/'] path-component ['/' path-component]*
// domain := domain-component ['.' domain-component]* [':' port-number]
// name := [domain '/'] remote-name
// domain := host [':' port-number]
// host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
// domain-name := domain-component ['.' domain-component]*
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// path (or "remote-name") := path-component ['/' path-component]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
@@ -21,7 +24,6 @@
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package reference
import (
@@ -33,8 +35,13 @@ import (
)
const (
// RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name.
RepositoryNameTotalLengthMax = 255
// NameTotalLengthMax is the maximum total number of characters in a repository name.
NameTotalLengthMax = 255
//
// Deprecated: use [RepositoryNameTotalLengthMax] instead.
NameTotalLengthMax = RepositoryNameTotalLengthMax
)
var (
@@ -53,8 +60,8 @@ var (
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameTooLong is returned when a repository name is longer than RepositoryNameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
@@ -145,7 +152,7 @@ type namedRepository interface {
Path() string
}
// Domain returns the domain part of the Named reference
// Domain returns the domain part of the [Named] reference.
func Domain(named Named) string {
if r, ok := named.(namedRepository); ok {
return r.Domain()
@@ -154,7 +161,7 @@ func Domain(named Named) string {
return domain
}
// Path returns the name without the domain part of the Named reference
// Path returns the name without the domain part of the [Named] reference.
func Path(named Named) (name string) {
if r, ok := named.(namedRepository); ok {
return r.Path()
@@ -163,6 +170,9 @@ func Path(named Named) (name string) {
return path
}
// splitDomain splits a named reference into a hostname and path string.
// If no valid hostname is found, the hostname is empty and the full value
// is returned as name
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
@@ -171,21 +181,8 @@ func splitDomain(name string) (string, string) {
return match[1], match[2]
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) {
if r, ok := named.(namedRepository); ok {
return r.Domain(), r.Path()
}
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
@@ -198,10 +195,6 @@ func Parse(s string) (Reference, error) {
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
@@ -213,6 +206,10 @@ func Parse(s string) (Reference, error) {
repo.path = matches[1]
}
if len(repo.path) > RepositoryNameTotalLengthMax {
return nil, ErrNameTooLong
}
ref := reference{
namedRepository: repo,
tag: matches[2],
@@ -237,7 +234,6 @@ func Parse(s string) (Reference, error) {
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
named, err := ParseNormalizedNamed(s)
if err != nil {
@@ -252,14 +248,15 @@ func ParseNamed(s string) (Named, error) {
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
}
if len(match[2]) > RepositoryNameTotalLengthMax {
return nil, ErrNameTooLong
}
return repository{
domain: match[1],
path: match[2],
@@ -320,11 +317,13 @@ func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
// TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named {
domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
repo := repository{}
if r, ok := ref.(namedRepository); ok {
repo.domain, repo.path = r.Domain(), r.Path()
} else {
repo.domain, repo.path = splitDomain(ref.Name())
}
return repo
}
func getBestReferenceType(ref reference) Reference {
+163
View File
@@ -0,0 +1,163 @@
package reference
import (
"regexp"
"strings"
)
// DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
var DigestRegexp = regexp.MustCompile(digestPat)
// DomainRegexp matches hostname or IP-addresses, optionally including a port
// number. It defines the structure of potential domain components that may be
// part of image names. This is purposely a subset of what is allowed by DNS to
// ensure backwards compatibility with Docker image names. It may be a subset of
// DNS domain name, an IPv4 address in decimal format, or an IPv6 address between
// square brackets (excluding zone identifiers as defined by [RFC 6874] or special
// addresses such as IPv4-Mapped).
//
// [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874.
var DomainRegexp = regexp.MustCompile(domainAndPort)
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
var IdentifierRegexp = regexp.MustCompile(identifier)
// NameRegexp is the format for the name component of references, including
// an optional domain and port, but without tag or digest suffix.
var NameRegexp = regexp.MustCompile(namePat)
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
var ReferenceRegexp = regexp.MustCompile(referencePat)
// TagRegexp matches valid tag names. From [docker/docker:graph/tags.go].
//
// [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28
var TagRegexp = regexp.MustCompile(tag)
const (
// alphanumeric defines the alphanumeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphanumeric = `[a-z0-9]+`
// separator defines the separators allowed to be embedded in name
// components. This allows one period, one or two underscore and multiple
// dashes. Repeated dashes and underscores are intentionally treated
// differently. In order to support valid hostnames as name components,
// supporting repeated dash was added. Additionally double underscore is
// now allowed as a separator to loosen the restriction for previously
// supported names.
separator = `(?:[._]|__|[-]+)`
// localhost is treated as a special value for domain-name. Any other
// domain-name without a "." or a ":port" are considered a path component.
localhost = `localhost`
// domainNameComponent restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp.
domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
// optionalPort matches an optional port-number including the port separator
// (e.g. ":80").
optionalPort = `(?::[0-9]+)?`
// tag matches valid tag names. From docker/docker:graph/tags.go.
tag = `[\w][\w.-]{0,127}`
// digestPat matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
//
// TODO(thaJeztah): this should follow the same rules as https://pkg.go.dev/github.com/opencontainers/go-digest@v1.0.0#DigestRegexp
// so that go-digest defines the canonical format. Note that the go-digest is
// more relaxed:
// - it allows multiple algorithms (e.g. "sha256+b64:<encoded>") to allow
// future expansion of supported algorithms.
// - it allows the "<encoded>" value to use urlsafe base64 encoding as defined
// in [rfc4648, section 5].
//
// [rfc4648, section 5]: https://www.rfc-editor.org/rfc/rfc4648#section-5.
digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
// identifier is the format for a content addressable identifier using sha256.
// These identifiers are like digests without the algorithm, since sha256 is used.
identifier = `([a-f0-9]{64})`
// ipv6address are enclosed between square brackets and may be represented
// in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
// are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
// IPv4-Mapped are deliberately excluded.
ipv6address = `\[(?:[a-fA-F0-9:]+)\]`
)
var (
// domainName defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names. This includes IPv4 addresses on decimal format.
domainName = domainNameComponent + anyTimes(`\.`+domainNameComponent)
// host defines the structure of potential domains based on the URI
// Host subcomponent on rfc3986. It may be a subset of DNS domain name,
// or an IPv4 address in decimal format, or an IPv6 address between square
// brackets (excluding zone identifiers as defined by rfc6874 or special
// addresses such as IPv4-Mapped).
host = `(?:` + domainName + `|` + ipv6address + `)`
// allowed by the URI Host subcomponent on rfc3986 to ensure backwards
// compatibility with Docker image names.
domainAndPort = host + optionalPort
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = regexp.MustCompile(anchored(tag))
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = regexp.MustCompile(anchored(digestPat))
// pathComponent restricts path-components to start with an alphanumeric
// character, with following parts able to be separated by a separator
// (one period, one or two underscore and multiple dashes).
pathComponent = alphanumeric + anyTimes(separator+alphanumeric)
// remoteName matches the remote-name of a repository. It consists of one
// or more forward slash (/) delimited path-components:
//
// pathComponent[[/pathComponent] ...] // e.g., "library/ubuntu"
remoteName = pathComponent + anyTimes(`/`+pathComponent)
namePat = optional(domainAndPort+`/`) + remoteName
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = regexp.MustCompile(anchored(optional(capture(domainAndPort), `/`), capture(remoteName)))
referencePat = anchored(capture(namePat), optional(`:`, capture(tag)), optional(`@`, capture(digestPat)))
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = regexp.MustCompile(anchored(identifier))
)
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...string) string {
return `(?:` + strings.Join(res, "") + `)?`
}
// anyTimes wraps the expression in a non-capturing group that can occur
// any number of times.
func anyTimes(res ...string) string {
return `(?:` + strings.Join(res, "") + `)*`
}
// capture wraps the expression in a capturing group.
func capture(res ...string) string {
return `(` + strings.Join(res, "") + `)`
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...string) string {
return `^` + strings.Join(res, "") + `$`
}
+75
View File
@@ -0,0 +1,75 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package reference
import (
"sort"
)
// Sort sorts string references preferring higher information references.
//
// The precedence is as follows:
//
// 1. [Named] + [Tagged] + [Digested] (e.g., "docker.io/library/busybox:latest@sha256:<digest>")
// 2. [Named] + [Tagged] (e.g., "docker.io/library/busybox:latest")
// 3. [Named] + [Digested] (e.g., "docker.io/library/busybo@sha256:<digest>")
// 4. [Named] (e.g., "docker.io/library/busybox")
// 5. [Digested] (e.g., "docker.io@sha256:<digest>")
// 6. Parse error
func Sort(references []string) []string {
var prefs []Reference
var bad []string
for _, ref := range references {
pref, err := ParseAnyReference(ref)
if err != nil {
bad = append(bad, ref)
} else {
prefs = append(prefs, pref)
}
}
sort.Slice(prefs, func(a, b int) bool {
ar := refRank(prefs[a])
br := refRank(prefs[b])
if ar == br {
return prefs[a].String() < prefs[b].String()
}
return ar < br
})
sort.Strings(bad)
var refs []string
for _, pref := range prefs {
refs = append(refs, pref.String())
}
return append(refs, bad...)
}
func refRank(ref Reference) uint8 {
if _, ok := ref.(Named); ok {
if _, ok = ref.(Tagged); ok {
if _, ok = ref.(Digested); ok {
return 1
}
return 2
}
if _, ok = ref.(Digested); ok {
return 3
}
return 4
}
return 5
}
-247
View File
@@ -1,247 +0,0 @@
package digestset
import (
"errors"
"sort"
"strings"
"sync"
digest "github.com/opencontainers/go-digest"
)
var (
// ErrDigestNotFound is used when a matching digest
// could not be found in a set.
ErrDigestNotFound = errors.New("digest not found")
// ErrDigestAmbiguous is used when multiple digests
// are found in a set. None of the matching digests
// should be considered valid matches.
ErrDigestAmbiguous = errors.New("ambiguous digest string")
)
// Set is used to hold a unique set of digests which
// may be easily referenced by easily referenced by a string
// representation of the digest as well as short representation.
// The uniqueness of the short representation is based on other
// digests in the set. If digests are omitted from this set,
// collisions in a larger set may not be detected, therefore it
// is important to always do short representation lookups on
// the complete set of digests. To mitigate collisions, an
// appropriately long short code should be used.
type Set struct {
mutex sync.RWMutex
entries digestEntries
}
// NewSet creates an empty set of digests
// which may have digests added.
func NewSet() *Set {
return &Set{
entries: digestEntries{},
}
}
// checkShortMatch checks whether two digests match as either whole
// values or short values. This function does not test equality,
// rather whether the second value could match against the first
// value.
func checkShortMatch(alg digest.Algorithm, hex, shortAlg, shortHex string) bool {
if len(hex) == len(shortHex) {
if hex != shortHex {
return false
}
if len(shortAlg) > 0 && string(alg) != shortAlg {
return false
}
} else if !strings.HasPrefix(hex, shortHex) {
return false
} else if len(shortAlg) > 0 && string(alg) != shortAlg {
return false
}
return true
}
// Lookup looks for a digest matching the given string representation.
// If no digests could be found ErrDigestNotFound will be returned
// with an empty digest value. If multiple matches are found
// ErrDigestAmbiguous will be returned with an empty digest value.
func (dst *Set) Lookup(d string) (digest.Digest, error) {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
if len(dst.entries) == 0 {
return "", ErrDigestNotFound
}
var (
searchFunc func(int) bool
alg digest.Algorithm
hex string
)
dgst, err := digest.Parse(d)
if err == digest.ErrDigestInvalidFormat {
hex = d
searchFunc = func(i int) bool {
return dst.entries[i].val >= d
}
} else {
hex = dgst.Hex()
alg = dgst.Algorithm()
searchFunc = func(i int) bool {
if dst.entries[i].val == hex {
return dst.entries[i].alg >= alg
}
return dst.entries[i].val >= hex
}
}
idx := sort.Search(len(dst.entries), searchFunc)
if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) {
return "", ErrDigestNotFound
}
if dst.entries[idx].alg == alg && dst.entries[idx].val == hex {
return dst.entries[idx].digest, nil
}
if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) {
return "", ErrDigestAmbiguous
}
return dst.entries[idx].digest, nil
}
// Add adds the given digest to the set. An error will be returned
// if the given digest is invalid. If the digest already exists in the
// set, this operation will be a no-op.
func (dst *Set) Add(d digest.Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
return dst.entries[i].alg >= entry.alg
}
return dst.entries[i].val >= entry.val
}
idx := sort.Search(len(dst.entries), searchFunc)
if idx == len(dst.entries) {
dst.entries = append(dst.entries, entry)
return nil
} else if dst.entries[idx].digest == d {
return nil
}
entries := append(dst.entries, nil)
copy(entries[idx+1:], entries[idx:len(entries)-1])
entries[idx] = entry
dst.entries = entries
return nil
}
// Remove removes the given digest from the set. An err will be
// returned if the given digest is invalid. If the digest does
// not exist in the set, this operation will be a no-op.
func (dst *Set) Remove(d digest.Digest) error {
if err := d.Validate(); err != nil {
return err
}
dst.mutex.Lock()
defer dst.mutex.Unlock()
entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d}
searchFunc := func(i int) bool {
if dst.entries[i].val == entry.val {
return dst.entries[i].alg >= entry.alg
}
return dst.entries[i].val >= entry.val
}
idx := sort.Search(len(dst.entries), searchFunc)
// Not found if idx is after or value at idx is not digest
if idx == len(dst.entries) || dst.entries[idx].digest != d {
return nil
}
entries := dst.entries
copy(entries[idx:], entries[idx+1:])
entries = entries[:len(entries)-1]
dst.entries = entries
return nil
}
// All returns all the digests in the set
func (dst *Set) All() []digest.Digest {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
retValues := make([]digest.Digest, len(dst.entries))
for i := range dst.entries {
retValues[i] = dst.entries[i].digest
}
return retValues
}
// ShortCodeTable returns a map of Digest to unique short codes. The
// length represents the minimum value, the maximum length may be the
// entire value of digest if uniqueness cannot be achieved without the
// full value. This function will attempt to make short codes as short
// as possible to be unique.
func ShortCodeTable(dst *Set, length int) map[digest.Digest]string {
dst.mutex.RLock()
defer dst.mutex.RUnlock()
m := make(map[digest.Digest]string, len(dst.entries))
l := length
resetIdx := 0
for i := 0; i < len(dst.entries); i++ {
var short string
extended := true
for extended {
extended = false
if len(dst.entries[i].val) <= l {
short = dst.entries[i].digest.String()
} else {
short = dst.entries[i].val[:l]
for j := i + 1; j < len(dst.entries); j++ {
if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) {
if j > resetIdx {
resetIdx = j
}
extended = true
} else {
break
}
}
if extended {
l++
}
}
}
m[dst.entries[i].digest] = short
if i >= resetIdx {
l = length
}
}
return m
}
type digestEntry struct {
alg digest.Algorithm
val string
digest digest.Digest
}
type digestEntries []*digestEntry
func (d digestEntries) Len() int {
return len(d)
}
func (d digestEntries) Less(i, j int) bool {
if d[i].val != d[j].val {
return d[i].val < d[j].val
}
return d[i].alg < d[j].alg
}
func (d digestEntries) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}
-199
View File
@@ -1,199 +0,0 @@
package reference
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/digestset"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "library"
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, errors.New("invalid reference format: repository name must be lowercase")
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// ParseDockerRef normalizes the image reference following the docker convention. This is added
// mainly for backward compatibility.
// The reference returned can only be either tagged or digested. For reference contains both tag
// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
func ParseDockerRef(ref string) (Named, error) {
named, err := ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, ok := named.(NamedTagged); ok {
if canonical, ok := named.(Canonical); ok {
// The reference is both tagged and digested, only
// return digested.
newNamed, err := WithName(canonical.Name())
if err != nil {
return nil, err
}
newCanonical, err := WithDigest(newNamed, canonical.Digest())
if err != nil {
return nil, err
}
return newCanonical, nil
}
}
return TagNameOnly(named), nil
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoName + "/" + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}
// ParseAnyReferenceWithSet parses a reference string as a possible short
// identifier to be matched in a digest set, a full digest, or familiar name.
func ParseAnyReferenceWithSet(ref string, ds *digestset.Set) (Reference, error) {
if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok {
dgst, err := ds.Lookup(ref)
if err == nil {
return digestReference(dgst), nil
}
} else {
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
}
return ParseNormalizedNamed(ref)
}
-143
View File
@@ -1,143 +0,0 @@
package reference
import "regexp"
var (
// alphaNumericRegexp defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumericRegexp = match(`[a-z0-9]+`)
// separatorRegexp defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes.
separatorRegexp = match(`(?:[._]|__|[-]*)`)
// nameComponentRegexp restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp
// and followed by an optional port.
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// DomainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
DomainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = match(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = anchored(TagRegexp)
// DigestRegexp matches valid digests.
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
optional(DomainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = anchored(
optional(capture(DomainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp)))
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = match(`([a-f0-9]{64})`)
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = anchored(IdentifierRegexp)
// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)
)
// match compiles the string to a regular expression.
var match = regexp.MustCompile
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) *regexp.Regexp {
re := match(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `?`)
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `+`)
}
// group wraps the regexp in a non-capturing group.
func group(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(?:` + expression(res...).String() + `)`)
}
// capture wraps the expression in a capturing group.
func capture(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(` + expression(res...).String() + `)`)
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
return match(`^` + expression(res...).String() + `$`)
}
+44 -5
View File
@@ -27,6 +27,7 @@ Adam Miller <admiller@redhat.com>
Adam Mills <adam@armills.info>
Adam Pointer <adam.pointer@skybettingandgaming.com>
Adam Singer <financeCoding@gmail.com>
Adam Thornton <adam.thornton@maryville.com>
Adam Walz <adam@adamwalz.net>
Adam Williams <awilliams@mirantis.com>
AdamKorcz <adam@adalogics.com>
@@ -173,6 +174,7 @@ Andy Rothfusz <github@developersupport.net>
Andy Smith <github@anarkystic.com>
Andy Wilson <wilson.andrew.j+github@gmail.com>
Andy Zhang <andy.zhangtao@hotmail.com>
Aneesh Kulkarni <askthefactorcamera@gmail.com>
Anes Hasicic <anes.hasicic@gmail.com>
Angel Velazquez <angelcar@amazon.com>
Anil Belur <askb23@gmail.com>
@@ -236,6 +238,7 @@ Ben Golub <ben.golub@dotcloud.com>
Ben Gould <ben@bengould.co.uk>
Ben Hall <ben@benhall.me.uk>
Ben Langfeld <ben@langfeld.me>
Ben Lovy <ben@deciduously.com>
Ben Sargent <ben@brokendigits.com>
Ben Severson <BenSeverson@users.noreply.github.com>
Ben Toews <mastahyeti@gmail.com>
@@ -262,7 +265,7 @@ Billy Ridgway <wrridgwa@us.ibm.com>
Bily Zhang <xcoder@tenxcloud.com>
Bin Liu <liubin0329@gmail.com>
Bingshen Wang <bingshen.wbs@alibaba-inc.com>
Bjorn Neergaard <bneergaard@mirantis.com>
Bjorn Neergaard <bjorn@neersighted.com>
Blake Geno <blakegeno@gmail.com>
Boaz Shuster <ripcurld.github@gmail.com>
bobby abbott <ttobbaybbob@gmail.com>
@@ -279,6 +282,7 @@ Brandon Liu <bdon@bdon.org>
Brandon Philips <brandon.philips@coreos.com>
Brandon Rhodes <brandon@rhodesmill.org>
Brendan Dixon <brendand@microsoft.com>
Brennan Kinney <5098581+polarathene@users.noreply.github.com>
Brent Salisbury <brent.salisbury@docker.com>
Brett Higgins <brhiggins@arbor.net>
Brett Kochendorfer <brett.kochendorfer@gmail.com>
@@ -363,6 +367,7 @@ chenyuzhu <chenyuzhi@oschina.cn>
Chetan Birajdar <birajdar.chetan@gmail.com>
Chewey <prosto-chewey@users.noreply.github.com>
Chia-liang Kao <clkao@clkao.org>
Chiranjeevi Tirunagari <vchiranjeeviak.tirunagari@gmail.com>
chli <chli@freewheel.tv>
Cholerae Hu <choleraehyq@gmail.com>
Chris Alfonso <calfonso@redhat.com>
@@ -433,8 +438,8 @@ Cristian Staretu <cristian.staretu@gmail.com>
cristiano balducci <cristiano.balducci@gmail.com>
Cristina Yenyxe Gonzalez Garcia <cristina.yenyxe@gmail.com>
Cruceru Calin-Cristian <crucerucalincristian@gmail.com>
cui fliter <imcusg@gmail.com>
CUI Wei <ghostplant@qq.com>
cuishuang <imcusg@gmail.com>
Cuong Manh Le <cuong.manhle.vn@gmail.com>
Cyprian Gracz <cyprian.gracz@micro-jumbo.eu>
Cyril F <cyrilf7x@gmail.com>
@@ -513,6 +518,7 @@ David Dooling <dooling@gmail.com>
David Gageot <david@gageot.net>
David Gebler <davidgebler@gmail.com>
David Glasser <glasser@davidglasser.net>
David Karlsson <35727626+dvdksn@users.noreply.github.com>
David Lawrence <david.lawrence@docker.com>
David Lechner <david@lechnology.com>
David M. Karr <davidmichaelkarr@gmail.com>
@@ -602,6 +608,7 @@ Donald Huang <don.hcd@gmail.com>
Dong Chen <dongluo.chen@docker.com>
Donghwa Kim <shanytt@gmail.com>
Donovan Jones <git@gamma.net.nz>
Dorin Geman <dorin.geman@docker.com>
Doron Podoleanu <doronp@il.ibm.com>
Doug Davis <dug@us.ibm.com>
Doug MacEachern <dougm@vmware.com>
@@ -636,6 +643,7 @@ Emily Rose <emily@contactvibe.com>
Emir Ozer <emirozer@yandex.com>
Eng Zer Jun <engzerjun@gmail.com>
Enguerran <engcolson@gmail.com>
Enrico Weigelt, metux IT consult <info@metux.net>
Eohyung Lee <liquidnuker@gmail.com>
epeterso <epeterson@breakpoint-labs.com>
er0k <er0k@er0k.net>
@@ -676,6 +684,7 @@ Evan Allrich <evan@unguku.com>
Evan Carmi <carmi@users.noreply.github.com>
Evan Hazlett <ejhazlett@gmail.com>
Evan Krall <krall@yelp.com>
Evan Lezar <elezar@nvidia.com>
Evan Phoenix <evan@fallingsnow.net>
Evan Wies <evan@neomantra.net>
Evelyn Xu <evelynhsu21@gmail.com>
@@ -744,6 +753,7 @@ Frank Groeneveld <frank@ivaldi.nl>
Frank Herrmann <fgh@4gh.tv>
Frank Macreery <frank@macreery.com>
Frank Rosquin <frank.rosquin+github@gmail.com>
Frank Villaro-Dixon <frank.villarodixon@merkle.com>
Frank Yang <yyb196@gmail.com>
Fred Lifton <fred.lifton@docker.com>
Frederick F. Kautz IV <fkautz@redhat.com>
@@ -983,6 +993,7 @@ Jean Rouge <rougej+github@gmail.com>
Jean-Baptiste Barth <jeanbaptiste.barth@gmail.com>
Jean-Baptiste Dalido <jeanbaptiste@appgratis.com>
Jean-Christophe Berthon <huygens@berthon.eu>
Jean-Michel Rouet <jm.rouet@gmail.com>
Jean-Paul Calderone <exarkun@twistedmatrix.com>
Jean-Pierre Huynh <jean-pierre.huynh@ounet.fr>
Jean-Tiare Le Bigot <jt@yadutaf.fr>
@@ -1013,6 +1024,7 @@ Jeroen Jacobs <github@jeroenj.be>
Jesse Dearing <jesse.dearing@gmail.com>
Jesse Dubay <jesse@thefortytwo.net>
Jessica Frazelle <jess@oxide.computer>
Jeyanthinath Muthuram <jeyanthinath10@gmail.com>
Jezeniel Zapanta <jpzapanta22@gmail.com>
Jhon Honce <jhonce@redhat.com>
Ji.Zhilong <zhilongji@gmail.com>
@@ -1141,6 +1153,7 @@ junxu <xujun@cmss.chinamobile.com>
Jussi Nummelin <jussi.nummelin@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com>
Justen Martin <jmart@the-coder.com>
Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com>
Justin Force <justin.force@gmail.com>
Justin Keller <85903732+jk-vb@users.noreply.github.com>
@@ -1183,6 +1196,7 @@ Ke Xu <leonhartx.k@gmail.com>
Kei Ohmura <ohmura.kei@gmail.com>
Keith Hudgins <greenman@greenman.org>
Keli Hu <dev@keli.hu>
Ken Bannister <kb2ma@runbox.com>
Ken Cochrane <kencochrane@gmail.com>
Ken Herner <kherner@progress.com>
Ken ICHIKAWA <ichikawa.ken@jp.fujitsu.com>
@@ -1192,7 +1206,7 @@ Kenjiro Nakayama <nakayamakenjiro@gmail.com>
Kent Johnson <kentoj@gmail.com>
Kenta Tada <Kenta.Tada@sony.com>
Kevin "qwazerty" Houdebert <kevin.houdebert@gmail.com>
Kevin Alvarez <crazy-max@users.noreply.github.com>
Kevin Alvarez <github@crazymax.dev>
Kevin Burke <kev@inburke.com>
Kevin Clark <kevin.clark@gmail.com>
Kevin Feyrer <kevin.feyrer@btinternet.com>
@@ -1225,6 +1239,7 @@ Konstantin Gribov <grossws@gmail.com>
Konstantin L <sw.double@gmail.com>
Konstantin Pelykh <kpelykh@zettaset.com>
Kostadin Plachkov <k.n.plachkov@gmail.com>
kpcyrd <git@rxv.cc>
Krasi Georgiev <krasi@vip-consult.solutions>
Krasimir Georgiev <support@vip-consult.co.uk>
Kris-Mikael Krister <krismikael@protonmail.com>
@@ -1306,6 +1321,7 @@ Lorenzo Fontana <fontanalorenz@gmail.com>
Lotus Fenn <fenn.lotus@gmail.com>
Louis Delossantos <ldelossa.ld@gmail.com>
Louis Opter <kalessin@kalessin.fr>
Luboslav Pivarc <lpivarc@redhat.com>
Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com>
Luca Orlandi <luca.orlandi@gmail.com>
@@ -1344,6 +1360,7 @@ Manuel Meurer <manuel@krautcomputing.com>
Manuel Rüger <manuel@rueg.eu>
Manuel Woelker <github@manuel.woelker.org>
mapk0y <mapk0y@gmail.com>
Marat Radchenko <marat@slonopotamus.org>
Marc Abramowitz <marc@marc-abramowitz.com>
Marc Kuo <kuomarc2@gmail.com>
Marc Tamsky <mtamsky@gmail.com>
@@ -1383,6 +1400,7 @@ Martijn van Oosterhout <kleptog@svana.org>
Martin Braun <braun@neuroforge.de>
Martin Dojcak <martin.dojcak@lablabs.io>
Martin Honermeyer <maze@strahlungsfrei.de>
Martin Jirku <martin@jirku.sk>
Martin Kelly <martin@surround.io>
Martin Mosegaard Amdisen <martin.amdisen@praqma.com>
Martin Muzatko <martin@happy-css.com>
@@ -1461,6 +1479,7 @@ Michael Holzheu <holzheu@linux.vnet.ibm.com>
Michael Hudson-Doyle <michael.hudson@canonical.com>
Michael Huettermann <michael@huettermann.net>
Michael Irwin <mikesir87@gmail.com>
Michael Kebe <michael.kebe@hkm.de>
Michael Kuehn <micha@kuehn.io>
Michael Käufl <docker@c.michael-kaeufl.de>
Michael Neale <michael.neale@gmail.com>
@@ -1509,10 +1528,11 @@ Mike Lundy <mike@fluffypenguin.org>
Mike MacCana <mike.maccana@gmail.com>
Mike Naberezny <mike@naberezny.com>
Mike Snitzer <snitzer@redhat.com>
Mike Sul <mike.sul@foundries.io>
mikelinjie <294893458@qq.com>
Mikhail Sobolev <mss@mawhrin.net>
Miklos Szegedi <miklos.szegedi@cloudera.com>
Milas Bowman <milasb@gmail.com>
Milas Bowman <devnull@milas.dev>
Milind Chawre <milindchawre@gmail.com>
Miloslav Trmač <mitr@redhat.com>
mingqing <limingqing@cyou-inc.com>
@@ -1524,6 +1544,7 @@ mlarcher <github@ringabell.org>
Mohammad Banikazemi <MBanikazemi@gmail.com>
Mohammad Nasirifar <farnasirim@gmail.com>
Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohd Sadiq <mohdsadiq058@gmail.com>
Mohit Soni <mosoni@ebay.com>
Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com>
@@ -1606,6 +1627,7 @@ Noah Treuhaft <noah.treuhaft@docker.com>
NobodyOnSE <ich@sektor.selfip.com>
noducks <onemannoducks@gmail.com>
Nolan Darilek <nolan@thewordnerd.info>
Nolan Miles <nolanpmiles@gmail.com>
Noriki Nakamura <noriki.nakamura@miraclelinux.com>
nponeccop <andy.melnikov@gmail.com>
Nurahmadie <nurahmadie@gmail.com>
@@ -1661,6 +1683,7 @@ Paul Lietar <paul@lietar.net>
Paul Liljenberg <liljenberg.paul@gmail.com>
Paul Morie <pmorie@gmail.com>
Paul Nasrat <pnasrat@gmail.com>
Paul Seiffert <paul.seiffert@jimdo.com>
Paul Weaver <pauweave@cisco.com>
Paulo Gomes <pjbgf@linux.com>
Paulo Ribeiro <paigr.io@gmail.com>
@@ -1674,6 +1697,7 @@ Pavlos Ratis <dastergon@gentoo.org>
Pavol Vargovcik <pallly.vargovcik@gmail.com>
Pawel Konczalski <mail@konczalski.de>
Paweł Gronowski <pawel.gronowski@docker.com>
payall4u <payall4u@qq.com>
Peeyush Gupta <gpeeyush@linux.vnet.ibm.com>
Peggy Li <peggyli.224@gmail.com>
Pei Su <sillyousu@gmail.com>
@@ -1703,7 +1727,9 @@ Phil Estes <estesp@gmail.com>
Phil Sphicas <phil.sphicas@att.com>
Phil Spitler <pspitler@gmail.com>
Philip Alexander Etling <paetling@gmail.com>
Philip K. Warren <pkwarren@gmail.com>
Philip Monroe <phil@philmonroe.com>
Philipp Fruck <dev@p-fruck.de>
Philipp Gillé <philipp.gille@gmail.com>
Philipp Wahala <philipp.wahala@gmail.com>
Philipp Weissensteiner <mail@philippweissensteiner.com>
@@ -1741,6 +1767,7 @@ Quentin Brossard <qbrossard@gmail.com>
Quentin Perez <qperez@ocs.online.net>
Quentin Tayssier <qtayssier@gmail.com>
r0n22 <cameron.regan@gmail.com>
Rachit Sharma <rachitsharma613@gmail.com>
Radostin Stoyanov <rstoyanov1@gmail.com>
Rafal Jeczalik <rjeczalik@gmail.com>
Rafe Colton <rafael.colton@gmail.com>
@@ -1773,6 +1800,7 @@ Rich Horwood <rjhorwood@apple.com>
Rich Moyse <rich@moyse.us>
Rich Seymour <rseymour@gmail.com>
Richard Burnison <rburnison@ebay.com>
Richard Hansen <rhansen@rhansen.org>
Richard Harvey <richard@squarecows.com>
Richard Mathie <richard.mathie@amey.co.uk>
Richard Metzler <richard@paadee.com>
@@ -1788,6 +1816,7 @@ Ritesh H Shukla <sritesh@vmware.com>
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Rob Cowsill <42620235+rcowsill@users.noreply.github.com>
Rob Gulewich <rgulewich@netflix.com>
Rob Murray <rob.murray@docker.com>
Rob Vesse <rvesse@dotnetrdf.org>
Robert Bachmann <rb@robertbachmann.at>
Robert Bittle <guywithnose@gmail.com>
@@ -1869,6 +1898,7 @@ ryancooper7 <ryan.cooper7@gmail.com>
RyanDeng <sheldon.d1018@gmail.com>
Ryo Nakao <nakabonne@gmail.com>
Ryoga Saito <contact@proelbtn.com>
Régis Behmo <regis@behmo.com>
Rémy Greinhofer <remy.greinhofer@livelovely.com>
s. rannou <mxs@sbrk.org>
Sabin Basyal <sabin.basyal@gmail.com>
@@ -1885,6 +1915,7 @@ Sam J Sharpe <sam.sharpe@digital.cabinet-office.gov.uk>
Sam Neirinck <sam@samneirinck.com>
Sam Reis <sreis@atlassian.com>
Sam Rijs <srijs@airpost.net>
Sam Thibault <sam.thibault@docker.com>
Sam Whited <sam@samwhited.com>
Sambuddha Basu <sambuddhabasu1@gmail.com>
Sami Wagiaalla <swagiaal@redhat.com>
@@ -1908,6 +1939,7 @@ Satoshi Tagomori <tagomoris@gmail.com>
Scott Bessler <scottbessler@gmail.com>
Scott Collier <emailscottcollier@gmail.com>
Scott Johnston <scott@docker.com>
Scott Moser <smoser@brickies.net>
Scott Percival <scottp@lastyard.com>
Scott Stamp <scottstamp851@gmail.com>
Scott Walls <sawalls@umich.edu>
@@ -1923,6 +1955,7 @@ Sebastiaan van Steenis <mail@superseb.nl>
Sebastiaan van Stijn <github@gone.nl>
Sebastian Höffner <sebastian.hoeffner@mevis.fraunhofer.de>
Sebastian Radloff <sradloff23@gmail.com>
Sebastian Thomschke <sebthom@users.noreply.github.com>
Sebastien Goasguen <runseb@gmail.com>
Senthil Kumar Selvaraj <senthil.thecoder@gmail.com>
Senthil Kumaran <senthil@uthcode.com>
@@ -1996,6 +2029,7 @@ Stanislav Bondarenko <stanislav.bondarenko@gmail.com>
Stanislav Levin <slev@altlinux.org>
Steeve Morin <steeve.morin@gmail.com>
Stefan Berger <stefanb@linux.vnet.ibm.com>
Stefan Gehrig <stefan.gehrig.hn@googlemail.com>
Stefan J. Wernli <swernli@microsoft.com>
Stefan Praszalowicz <stefan@greplin.com>
Stefan S. <tronicum@user.github.com>
@@ -2003,6 +2037,7 @@ Stefan Scherer <stefan.scherer@docker.com>
Stefan Staudenmeyer <doerte@instana.com>
Stefan Weil <sw@weilnetz.de>
Steffen Butzer <steffen.butzer@outlook.com>
Stephan Henningsen <stephan-henningsen@users.noreply.github.com>
Stephan Spindler <shutefan@gmail.com>
Stephen Benjamin <stephen@redhat.com>
Stephen Crosby <stevecrozz@gmail.com>
@@ -2204,6 +2239,7 @@ Vinod Kulkarni <vinod.kulkarni@gmail.com>
Vishal Doshi <vishal.doshi@gmail.com>
Vishnu Kannan <vishnuk@google.com>
Vitaly Ostrosablin <vostrosablin@virtuozzo.com>
Vitor Anjos <bartier@users.noreply.github.com>
Vitor Monteiro <vmrmonteiro@gmail.com>
Vivek Agarwal <me@vivek.im>
Vivek Dasgupta <vdasgupt@redhat.com>
@@ -2250,6 +2286,7 @@ Wenxuan Zhao <viz@linux.com>
Wenyu You <21551128@zju.edu.cn>
Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com>
Wesley Pettit <wppttt@amazon.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
Wiktor Kwapisiewicz <wiktor@metacode.biz>
Will Dietz <w@wdtz.org>
@@ -2289,7 +2326,7 @@ xiekeyang <xiekeyang@huawei.com>
Ximo Guanter Gonzálbez <joaquin.guantergonzalbez@telefonica.com>
xin.li <xin.li@daocloud.io>
Xinbo Weng <xihuanbo_0521@zju.edu.cn>
Xinfeng Liu <xinfeng.liu@gmail.com>
Xinfeng Liu <XinfengLiu@icloud.com>
Xinzi Zhou <imdreamrunner@gmail.com>
Xiuming Chen <cc@cxm.cc>
Xuecong Liao <satorulogic@gmail.com>
@@ -2355,6 +2392,7 @@ Zen Lin(Zhinan Lin) <linzhinan@huawei.com>
Zhang Kun <zkazure@gmail.com>
Zhang Wei <zhangwei555@huawei.com>
Zhang Wentao <zhangwentao234@huawei.com>
zhangguanzhang <zhangguanzhang@qq.com>
ZhangHang <stevezhang2014@gmail.com>
zhangxianwei <xianwei.zw@alibaba-inc.com>
Zhenan Ye <21551168@zju.edu.cn>
@@ -2381,6 +2419,7 @@ Zuhayr Elahi <zuhayr.elahi@docker.com>
Zunayed Ali <zunayed@gmail.com>
Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com>
吴小白 <296015668@qq.com>
尹吉峰 <jifeng.yin@gmail.com>
屈骏 <qujun@tiduyun.com>
徐俊杰 <paco.xu@daocloud.io>
+1 -1
View File
@@ -37,6 +37,6 @@ There is hopefully enough example material in the file for you to copy a similar
When you make edits to `swagger.yaml`, you may want to check the generated API documentation to ensure it renders correctly.
Run `make swagger-docs` and a preview will be running at `http://localhost`. Some of the styling may be incorrect, but you'll be able to ensure that it is generating the correct documentation.
Run `make swagger-docs` and a preview will be running at `http://localhost:9000`. Some of the styling may be incorrect, but you'll be able to ensure that it is generating the correct documentation.
The production documentation is generated by vendoring `swagger.yaml` into [docker/docker.github.io](https://github.com/docker/docker.github.io).
+1 -1
View File
@@ -3,7 +3,7 @@ package api // import "github.com/docker/docker/api"
// Common constants for daemon and client.
const (
// DefaultVersion of Current REST API
DefaultVersion = "1.43"
DefaultVersion = "1.44"
// NoBaseImageSpecifier is the symbol used by the FROM
// command to specify that no base image is to be used.
-7
View File
@@ -1,7 +0,0 @@
//go:build !windows
// +build !windows
package api // import "github.com/docker/docker/api"
// MinVersion represents Minimum REST API version supported
const MinVersion = "1.12"
-8
View File
@@ -1,8 +0,0 @@
package api // import "github.com/docker/docker/api"
// MinVersion represents Minimum REST API version supported
// Technically the first daemon API version released on Windows is v1.25 in
// engine version 1.13. However, some clients are explicitly using downlevel
// APIs (e.g. docker-compose v2.1 file format) and that is just too restrictive.
// Hence also allowing 1.24 on Windows.
const MinVersion string = "1.24"
+274 -99
View File
@@ -19,10 +19,10 @@ produces:
consumes:
- "application/json"
- "text/plain"
basePath: "/v1.43"
basePath: "/v1.44"
info:
title: "Docker Engine API"
version: "1.43"
version: "1.44"
x-logo:
url: "https://docs.docker.com/assets/images/logo-docker-main.png"
description: |
@@ -55,8 +55,8 @@ info:
the URL is not supported by the daemon, a HTTP `400 Bad Request` error message
is returned.
If you omit the version-prefix, the current version of the API (v1.43) is used.
For example, calling `/info` is the same as calling `/v1.43/info`. Using the
If you omit the version-prefix, the current version of the API (v1.44) is used.
For example, calling `/info` is the same as calling `/v1.44/info`. Using the
API without a version-prefix is deprecated and will be removed in a future release.
Engine releases in the near future should support this version of the API,
@@ -388,6 +388,20 @@ definitions:
description: "Create mount point on host if missing"
type: "boolean"
default: false
ReadOnlyNonRecursive:
description: |
Make the mount non-recursively read-only, but still leave the mount recursive
(unless NonRecursive is set to `true` in conjunction).
Addded in v1.44, before that version all read-only mounts were
non-recursive by default. To match the previous behaviour this
will default to `true` for clients on versions prior to v1.44.
type: "boolean"
default: false
ReadOnlyForceRecursive:
description: "Raise an error if the mount cannot be made recursively read-only."
type: "boolean"
default: false
VolumeOptions:
description: "Optional configuration for the `volume` type."
type: "object"
@@ -794,6 +808,12 @@ definitions:
1000000 (1 ms). 0 means inherit.
type: "integer"
format: "int64"
StartInterval:
description: |
The time to wait between checks in nanoseconds during the start period.
It should be 0 or at least 1000000 (1 ms). 0 means inherit.
type: "integer"
format: "int64"
Health:
description: |
@@ -1297,7 +1317,10 @@ definitions:
type: "boolean"
x-nullable: true
MacAddress:
description: "MAC address of the container."
description: |
MAC address of the container.
Deprecated: this field is deprecated in API v1.44 and up. Use EndpointSettings.MacAddress instead.
type: "string"
x-nullable: true
OnBuild:
@@ -1347,16 +1370,16 @@ definitions:
EndpointsConfig:
description: |
A mapping of network name to endpoint configuration for that network.
The endpoint configuration can be left empty to connect to that
network with no particular endpoint configuration.
type: "object"
additionalProperties:
$ref: "#/definitions/EndpointSettings"
example:
# putting an example here, instead of using the example values from
# /definitions/EndpointSettings, because containers/create currently
# does not support attaching to multiple networks, so the example request
# would be confusing if it showed that multiple networks can be contained
# in the EndpointsConfig.
# TODO remove once we support multiple networks on container create (see https://github.com/moby/moby/blob/07e6b843594e061f82baa5fa23c2ff7d536c2a05/daemon/create.go#L323)
# /definitions/EndpointSettings, because EndpointSettings contains
# operational data returned when inspecting a container that we don't
# accept here.
EndpointsConfig:
isolated_nw:
IPAMConfig:
@@ -1365,19 +1388,22 @@ definitions:
LinkLocalIPs:
- "169.254.34.68"
- "fe80::3468"
MacAddress: "02:42:ac:12:05:02"
Links:
- "container_1"
- "container_2"
Aliases:
- "server_x"
- "server_y"
database_nw: {}
NetworkSettings:
description: "NetworkSettings exposes the network settings in the API"
type: "object"
properties:
Bridge:
description: Name of the network's bridge (for example, `docker0`).
description: |
Name of the default bridge interface when dockerd's --bridge flag is set.
type: "string"
example: "docker0"
SandboxID:
@@ -1387,34 +1413,40 @@ definitions:
HairpinMode:
description: |
Indicates if hairpin NAT should be enabled on the virtual interface.
Deprecated: This field is never set and will be removed in a future release.
type: "boolean"
example: false
LinkLocalIPv6Address:
description: IPv6 unicast address using the link-local prefix.
description: |
IPv6 unicast address using the link-local prefix.
Deprecated: This field is never set and will be removed in a future release.
type: "string"
example: "fe80::42:acff:fe11:1"
example: ""
LinkLocalIPv6PrefixLen:
description: Prefix length of the IPv6 unicast address.
description: |
Prefix length of the IPv6 unicast address.
Deprecated: This field is never set and will be removed in a future release.
type: "integer"
example: "64"
example: ""
Ports:
$ref: "#/definitions/PortMap"
SandboxKey:
description: SandboxKey identifies the sandbox
description: SandboxKey is the full path of the netns handle
type: "string"
example: "/var/run/docker/netns/8ab54b426c38"
# TODO is SecondaryIPAddresses actually used?
SecondaryIPAddresses:
description: ""
description: "Deprecated: This field is never set and will be removed in a future release."
type: "array"
items:
$ref: "#/definitions/Address"
x-nullable: true
# TODO is SecondaryIPv6Addresses actually used?
SecondaryIPv6Addresses:
description: ""
description: "Deprecated: This field is never set and will be removed in a future release."
type: "array"
items:
$ref: "#/definitions/Address"
@@ -1715,18 +1747,27 @@ definitions:
description: |
Date and time at which the image was created, formatted in
[RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds.
This information is only available if present in the image,
and omitted otherwise.
type: "string"
x-nullable: false
format: "dateTime"
x-nullable: true
example: "2022-02-04T21:20:12.497794809Z"
Container:
description: |
The ID of the container that was used to create the image.
Depending on how the image was created, this field may be empty.
**Deprecated**: this field is kept for backward compatibility, but
will be removed in API v1.45.
type: "string"
x-nullable: false
example: "65974bc86f1770ae4bff79f651ebdbce166ae9aada632ee3fa9af3a264911735"
ContainerConfig:
description: |
**Deprecated**: this field is kept for backward compatibility, but
will be removed in API v1.45.
$ref: "#/definitions/ContainerConfig"
DockerVersion:
description: |
@@ -1781,13 +1822,7 @@ definitions:
description: |
Total size of the image including all layers it is composed of.
In versions of Docker before v1.10, this field was calculated from
the image itself and all of its parent images. Images are now stored
self-contained, and no longer use a parent-chain, making this field
an equivalent of the Size field.
> **Deprecated**: this field is kept for backward compatibility, but
> will be removed in API v1.44.
Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead.
type: "integer"
format: "int64"
example: 1239828
@@ -1829,6 +1864,7 @@ definitions:
x-nullable: true
ImageSummary:
type: "object"
x-go-name: "Summary"
required:
- Id
- ParentId
@@ -1925,12 +1961,7 @@ definitions:
description: |-
Total size of the image including all layers it is composed of.
In versions of Docker before v1.10, this field was calculated from
the image itself and all of its parent images. Images are now stored
self-contained, and no longer use a parent-chain, making this field
an equivalent of the Size field.
Deprecated: this field is kept for backward compatibility, and will be removed in API v1.44.
Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead.
type: "integer"
format: "int64"
example: 172064416
@@ -2448,6 +2479,11 @@ definitions:
example:
- "container_1"
- "container_2"
MacAddress:
description: |
MAC address for the endpoint on this network. The network driver might ignore this parameter.
type: "string"
example: "02:42:ac:11:00:04"
Aliases:
type: "array"
items:
@@ -2498,11 +2534,6 @@ definitions:
type: "integer"
format: "int64"
example: 64
MacAddress:
description: |
MAC address for the endpoint on this network.
type: "string"
example: "02:42:ac:11:00:04"
DriverOpts:
description: |
DriverOpts is a mapping of driver options and values. These options
@@ -2514,6 +2545,21 @@ definitions:
example:
com.example.some-label: "some-value"
com.example.some-other-label: "some-other-value"
DNSNames:
description: |
List of all DNS names an endpoint has on a specific network. This
list is based on the container name, network aliases, container short
ID, and hostname.
These DNS names are non-fully qualified but can contain several dots.
You can get fully qualified DNS names by appending `.<network-name>`.
For instance, if container name is `my.ctr` and the network is named
`testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be
`my.ctr.testnet`.
type: array
items:
type: string
example: ["foobar", "server_x", "server_y", "my.ctr"]
EndpointIPAMConfig:
description: |
@@ -3545,6 +3591,32 @@ definitions:
Level:
type: "string"
description: "SELinux level label"
Seccomp:
type: "object"
description: "Options for configuring seccomp on the container"
properties:
Mode:
type: "string"
enum:
- "default"
- "unconfined"
- "custom"
Profile:
description: "The custom seccomp profile as a json object"
type: "string"
AppArmor:
type: "object"
description: "Options for configuring AppArmor on the container"
properties:
Mode:
type: "string"
enum:
- "default"
- "disabled"
NoNewPrivileges:
type: "boolean"
description: "Configuration of the no_new_privs bit in the container"
TTY:
description: "Whether a pseudo-TTY should be allocated."
type: "boolean"
@@ -3939,6 +4011,44 @@ definitions:
- "remove"
- "orphaned"
ContainerStatus:
type: "object"
description: "represents the status of a container."
properties:
ContainerID:
type: "string"
PID:
type: "integer"
ExitCode:
type: "integer"
PortStatus:
type: "object"
description: "represents the port status of a task's host ports whose service has published host ports"
properties:
Ports:
type: "array"
items:
$ref: "#/definitions/EndpointPortConfig"
TaskStatus:
type: "object"
description: "represents the status of a task."
properties:
Timestamp:
type: "string"
format: "dateTime"
State:
$ref: "#/definitions/TaskState"
Message:
type: "string"
Err:
type: "string"
ContainerStatus:
$ref: "#/definitions/ContainerStatus"
PortStatus:
$ref: "#/definitions/PortStatus"
Task:
type: "object"
properties:
@@ -3974,26 +4084,7 @@ definitions:
AssignedGenericResources:
$ref: "#/definitions/GenericResources"
Status:
type: "object"
properties:
Timestamp:
type: "string"
format: "dateTime"
State:
$ref: "#/definitions/TaskState"
Message:
type: "string"
Err:
type: "string"
ContainerStatus:
type: "object"
properties:
ContainerID:
type: "string"
PID:
type: "integer"
ExitCode:
type: "integer"
$ref: "#/definitions/TaskStatus"
DesiredState:
$ref: "#/definitions/TaskState"
JobIteration:
@@ -4209,7 +4300,10 @@ definitions:
- "stop-first"
- "start-first"
Networks:
description: "Specifies which networks the service should attach to."
description: |
Specifies which networks the service should attach to.
Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead.
type: "array"
items:
$ref: "#/definitions/NetworkAttachmentConfig"
@@ -4443,6 +4537,7 @@ definitions:
ImageDeleteResponseItem:
type: "object"
x-go-name: "DeleteResponse"
properties:
Untagged:
description: "The image ID of an image that was untagged"
@@ -4451,6 +4546,29 @@ definitions:
description: "The image ID of an image that was deleted"
type: "string"
ServiceCreateResponse:
type: "object"
description: |
contains the information returned to a client on the
creation of a new service.
properties:
ID:
description: "The ID of the created service."
type: "string"
x-nullable: false
example: "ak7w3gjqoa3kuz8xcpnyy0pvl"
Warnings:
description: |
Optional warning message.
FIXME(thaJeztah): this should have "omitempty" in the generated type.
type: "array"
x-nullable: true
items:
type: "string"
example:
- "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
ServiceUpdateResponse:
type: "object"
properties:
@@ -4460,7 +4578,8 @@ definitions:
items:
type: "string"
example:
Warning: "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
Warnings:
- "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
ContainerSummary:
type: "object"
@@ -4819,7 +4938,7 @@ definitions:
The version Go used to compile the daemon, and the version of the Go
runtime in use.
type: "string"
example: "go1.13.14"
example: "go1.21.12"
Os:
description: |
The operating system that the daemon is running on ("linux" or "windows")
@@ -5294,7 +5413,25 @@ definitions:
- "WARNING: No memory limit support"
- "WARNING: bridge-nf-call-iptables is disabled"
- "WARNING: bridge-nf-call-ip6tables is disabled"
CDISpecDirs:
description: |
List of directories where (Container Device Interface) CDI
specifications are located.
These specifications define vendor-specific modifications to an OCI
runtime specification for a container being created.
An empty list indicates that CDI device injection is disabled.
Note that since using CDI device injection requires the daemon to have
experimental enabled. For non-experimental daemons an empty list will
always be returned.
type: "array"
items:
type: "string"
example:
- "/etc/cdi"
- "/var/run/cdi"
# PluginsInfo is a temp struct holding Plugins name
# registered with docker daemon. It is used by Info struct
@@ -5530,6 +5667,28 @@ definitions:
items:
type: "string"
example: ["--debug", "--systemd-cgroup=false"]
status:
description: |
Information specific to the runtime.
While this API specification does not define data provided by runtimes,
the following well-known properties may be provided by runtimes:
`org.opencontainers.runtime-spec.features`: features structure as defined
in the [OCI Runtime Specification](https://github.com/opencontainers/runtime-spec/blob/main/features.md),
in a JSON string representation.
<p><br /></p>
> **Note**: The information returned in this field, including the
> formatting of values and labels, should not be considered stable,
> and may change without notice.
type: "object"
x-nullable: true
additionalProperties:
type: "string"
example:
"org.opencontainers.runtime-spec.features": "{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.1.0\",\"...\":\"...\"}"
Commit:
description: |
@@ -6414,6 +6573,7 @@ paths:
Aliases:
- "server_x"
- "server_y"
database_nw: {}
required: true
responses:
@@ -6561,7 +6721,7 @@ paths:
StopSignal: "SIGTERM"
StopTimeout: 10
Created: "2015-01-06T15:47:31.485331387Z"
Driver: "devicemapper"
Driver: "overlay2"
ExecIDs:
- "b35395de42bc8abd327f9dd65d913b9ba28c74d2f0734eeeae84fa1c616a0fca"
- "3fc1232e5cd20c8de182ed81178503dc6437f4e7ef12b52cc5e8de020652f1c4"
@@ -7992,6 +8152,7 @@ paths:
- `label=key` or `label="key=value"` of an image label
- `reference`=(`<image-name>[:<tag>]`)
- `since`=(`<image-name>[:<tag>]`, `<image id>` or `<image@digest>`)
- `until=<timestamp>`
type: "string"
- name: "shared-size"
in: "query"
@@ -8174,6 +8335,16 @@ paths:
description: "BuildKit output configuration"
type: "string"
default: ""
- name: "version"
in: "query"
type: "string"
default: "1"
enum: ["1", "2"]
description: |
Version of the builder backend to use.
- `1` is the first generation classic (deprecated) builder in the Docker daemon (default)
- `2` is [BuildKit](https://github.com/moby/buildkit)
responses:
200:
description: "no error"
@@ -8243,7 +8414,7 @@ paths:
/images/create:
post:
summary: "Create an image"
description: "Create an image by either pulling it from a registry or importing it."
description: "Pull or import an image."
operationId: "ImageCreate"
consumes:
- "text/plain"
@@ -8594,28 +8765,36 @@ paths:
is_official:
type: "boolean"
is_automated:
description: |
Whether this repository has automated builds enabled.
<p><br /></p>
> **Deprecated**: This field is deprecated and will always
> be "false" in future.
type: "boolean"
example: false
name:
type: "string"
star_count:
type: "integer"
examples:
application/json:
- description: ""
is_official: false
- description: "A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!"
is_official: true
is_automated: false
name: "wma55/u1210sshd"
star_count: 0
- description: ""
is_official: false
name: "alpine"
star_count: 10093
- description: "Busybox base image."
is_official: true
is_automated: false
name: "jdswinbank/sshd"
star_count: 0
- description: ""
is_official: false
name: "Busybox base image."
star_count: 3037
- description: "The PostgreSQL object-relational database system provides reliability and data integrity."
is_official: true
is_automated: false
name: "vgauthier/sshd"
star_count: 0
name: "postgres"
star_count: 12408
500:
description: "Server error"
schema:
@@ -8635,9 +8814,13 @@ paths:
description: |
A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters:
- `is-automated=(true|false)`
- `is-automated=(true|false)` (deprecated, see below)
- `is-official=(true|false)`
- `stars=<number>` Matches images that has at least 'number' stars.
The `is-automated` filter is deprecated. The `is_automated` field has
been deprecated by Docker Hub's search API. Consequently, searching
for `is-automated=true` will yield no results.
type: "string"
tags: ["Image"]
/images/prune:
@@ -9030,7 +9213,6 @@ paths:
Created: 1466724217
Size: 1092588
SharedSize: 0
VirtualSize: 1092588
Labels: {}
Containers: 1
Containers:
@@ -9893,6 +10075,10 @@ paths:
example:
Id: "22be93d5babb089c5aab8dbc369042fad48ff791584ca2da2100db837a1c7c30"
Warning: ""
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: |
Forbidden operation. This happens when trying to create a network named after a pre-defined network,
@@ -9922,13 +10108,7 @@ paths:
type: "string"
CheckDuplicate:
description: |
Check for networks with duplicate names. Since Network is
primarily keyed based on a random ID and not on the name, and
network name is strictly a user-friendly alias to the network
which is uniquely identified using ID, there is no guaranteed
way to check for duplicates. CheckDuplicate is there to provide
a best effort checking of any networks which has the same name
but it is not guaranteed to catch all name collisions.
Deprecated: CheckDuplicate is now always enabled.
type: "boolean"
Driver:
description: "Name of the network driver plugin to use."
@@ -9996,14 +10176,19 @@ paths:
/networks/{id}/connect:
post:
summary: "Connect a container to a network"
description: "The network must be either a local-scoped network or a swarm-scoped network with the `attachable` option set. A network cannot be re-attached to a running container"
operationId: "NetworkConnect"
consumes:
- "application/json"
responses:
200:
description: "No error"
400:
description: "bad parameter"
schema:
$ref: "#/definitions/ErrorResponse"
403:
description: "Operation not supported for swarm scoped networks"
description: "Operation forbidden"
schema:
$ref: "#/definitions/ErrorResponse"
404:
@@ -10038,6 +10223,7 @@ paths:
IPAMConfig:
IPv4Address: "172.24.56.89"
IPv6Address: "2001:db8::5689"
MacAddress: "02:42:ac:12:05:02"
tags: ["Network"]
/networks/{id}/disconnect:
@@ -11031,18 +11217,7 @@ paths:
201:
description: "no error"
schema:
type: "object"
title: "ServiceCreateResponse"
properties:
ID:
description: "The ID of the created service."
type: "string"
Warning:
description: "Optional warning message"
type: "string"
example:
ID: "ak7w3gjqoa3kuz8xcpnyy0pvl"
Warning: "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found"
$ref: "#/definitions/ServiceCreateResponse"
400:
description: "bad parameter"
schema:
-7
View File
@@ -1,7 +0,0 @@
package types // import "github.com/docker/docker/api/types"
import "github.com/docker/docker/api/types/registry"
// AuthConfig contains authorization information for connecting to a Registry.
//
// Deprecated: use github.com/docker/docker/api/types/registry.AuthConfig
type AuthConfig = registry.AuthConfig
+7
View File
@@ -0,0 +1,7 @@
package checkpoint
// Summary represents the details of a checkpoint when listing endpoints.
type Summary struct {
// Name is the name of the checkpoint.
Name string
}
+19
View File
@@ -0,0 +1,19 @@
package checkpoint
// CreateOptions holds parameters to create a checkpoint from a container.
type CreateOptions struct {
CheckpointID string
CheckpointDir string
Exit bool
}
// ListOptions holds parameters to list checkpoints for a container.
type ListOptions struct {
CheckpointDir string
}
// DeleteOptions holds parameters to delete a checkpoint from a container.
type DeleteOptions struct {
CheckpointID string
CheckpointDir string
}
-91
View File
@@ -11,44 +11,6 @@ import (
units "github.com/docker/go-units"
)
// CheckpointCreateOptions holds parameters to create a checkpoint from a container
type CheckpointCreateOptions struct {
CheckpointID string
CheckpointDir string
Exit bool
}
// CheckpointListOptions holds parameters to list checkpoints for a container
type CheckpointListOptions struct {
CheckpointDir string
}
// CheckpointDeleteOptions holds parameters to delete a checkpoint from a container
type CheckpointDeleteOptions struct {
CheckpointID string
CheckpointDir string
}
// ContainerAttachOptions holds parameters to attach to a container.
type ContainerAttachOptions struct {
Stream bool
Stdin bool
Stdout bool
Stderr bool
DetachKeys string
Logs bool
}
// ContainerCommitOptions holds parameters to commit changes into a container.
type ContainerCommitOptions struct {
Reference string
Comment string
Author string
Changes []string
Pause bool
Config *container.Config
}
// ContainerExecInspect holds information returned by exec inspect.
type ContainerExecInspect struct {
ExecID string `json:"ID"`
@@ -58,42 +20,6 @@ type ContainerExecInspect struct {
Pid int
}
// ContainerListOptions holds parameters to list containers with.
type ContainerListOptions struct {
Size bool
All bool
Latest bool
Since string
Before string
Limit int
Filters filters.Args
}
// ContainerLogsOptions holds parameters to filter logs with.
type ContainerLogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Timestamps bool
Follow bool
Tail string
Details bool
}
// ContainerRemoveOptions holds parameters to remove containers.
type ContainerRemoveOptions struct {
RemoveVolumes bool
RemoveLinks bool
Force bool
}
// ContainerStartOptions holds parameters to start containers.
type ContainerStartOptions struct {
CheckpointID string
CheckpointDir string
}
// CopyToContainerOptions holds information
// about files to copy into a container
type CopyToContainerOptions struct {
@@ -307,14 +233,6 @@ type ImageSearchOptions struct {
Limit int
}
// ResizeOptions holds parameters to resize a tty.
// It can be used to resize container ttys and
// exec process ttys too.
type ResizeOptions struct {
Height uint
Width uint
}
// NodeListOptions holds parameters to list nodes with.
type NodeListOptions struct {
Filters filters.Args
@@ -340,15 +258,6 @@ type ServiceCreateOptions struct {
QueryRegistry bool
}
// ServiceCreateResponse contains the information returned to a client
// on the creation of a new service.
type ServiceCreateResponse struct {
// ID is the ID of the created service.
ID string
// Warnings is a set of non-fatal warning messages to pass on to the user.
Warnings []string `json:",omitempty"`
}
// Values for RegistryAuthFrom in ServiceUpdateOptions
const (
RegistryAuthFromSpec = "spec"
-49
View File
@@ -1,32 +1,5 @@
package types // import "github.com/docker/docker/api/types"
import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// configs holds structs used for internal communication between the
// frontend (such as an http server) and the backend (such as the
// docker daemon).
// ContainerCreateConfig is the parameter set to ContainerCreate()
type ContainerCreateConfig struct {
Name string
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
Platform *ocispec.Platform
AdjustCPUShares bool
}
// ContainerRmConfig holds arguments for the container remove
// operation. This struct is used to tell the backend what operations
// to perform.
type ContainerRmConfig struct {
ForceRemove, RemoveVolume, RemoveLink bool
}
// ExecConfig is a small subset of the Config struct that holds the configuration
// for the exec feature of docker.
type ExecConfig struct {
@@ -43,25 +16,3 @@ type ExecConfig struct {
WorkingDir string // Working directory
Cmd []string // Execution commands and args
}
// PluginRmConfig holds arguments for plugin remove.
type PluginRmConfig struct {
ForceRemove bool
}
// PluginEnableConfig holds arguments for plugin enable
type PluginEnableConfig struct {
Timeout int
}
// PluginDisableConfig holds arguments for plugin disable.
type PluginDisableConfig struct {
ForceDisable bool
}
// NetworkListConfig stores the options available for listing networks
type NetworkListConfig struct {
// TODO(@cpuguy83): naming is hard, this is pulled from what was being used in the router before moving here
Detailed bool
Verbose bool
}
@@ -1,6 +0,0 @@
package container
// ContainerChangeResponseItem change item in response to ContainerChanges operation
//
// Deprecated: use [FilesystemChange].
type ContainerChangeResponseItem = FilesystemChange
+11 -25
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/docker/docker/api/types/strslice"
dockerspec "github.com/docker/docker/image/spec/specs-go/v1"
"github.com/docker/go-connections/nat"
)
@@ -33,25 +34,7 @@ type StopOptions struct {
}
// HealthConfig holds configuration settings for the HEALTHCHECK feature.
type HealthConfig struct {
// Test is the test to perform to check that the container is healthy.
// An empty slice means to inherit the default.
// The options are:
// {} : inherit healthcheck
// {"NONE"} : disable healthcheck
// {"CMD", args...} : exec arguments directly
// {"CMD-SHELL", command} : run command with system's default shell
Test []string `json:",omitempty"`
// Zero means to inherit. Durations are expressed as integer nanoseconds.
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down.
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
// Zero means inherit.
Retries int `json:",omitempty"`
}
type HealthConfig = dockerspec.HealthcheckConfig
// ExecStartOptions holds the options to start container's exec.
type ExecStartOptions struct {
@@ -87,10 +70,13 @@ type Config struct {
WorkingDir string // Current directory (PWD) in the command will be launched
Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
NetworkDisabled bool `json:",omitempty"` // Is network disabled
MacAddress string `json:",omitempty"` // Mac Address of the container
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
Labels map[string]string // List of labels set to this container
StopSignal string `json:",omitempty"` // Signal to stop a container
StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
// Mac Address of the container.
//
// Deprecated: this field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead.
MacAddress string `json:",omitempty"`
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
Labels map[string]string // List of labels set to this container
StopSignal string `json:",omitempty"` // Signal to stop a container
StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
}
+9
View File
@@ -0,0 +1,9 @@
package container
type errInvalidParameter struct{ error }
func (e *errInvalidParameter) InvalidParameter() {}
func (e *errInvalidParameter) Unwrap() error {
return e.error
}
+45 -7
View File
@@ -1,10 +1,12 @@
package container // import "github.com/docker/docker/api/types/container"
import (
"fmt"
"strings"
"github.com/docker/docker/api/types/blkiodev"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
@@ -132,12 +134,12 @@ type NetworkMode string
// IsNone indicates whether container isn't using a network stack.
func (n NetworkMode) IsNone() bool {
return n == "none"
return n == network.NetworkNone
}
// IsDefault indicates whether container uses the default network stack.
func (n NetworkMode) IsDefault() bool {
return n == "default"
return n == network.NetworkDefault
}
// IsPrivate indicates whether container uses its private network stack.
@@ -271,33 +273,42 @@ type DeviceMapping struct {
// RestartPolicy represents the restart policies of the container.
type RestartPolicy struct {
Name string
Name RestartPolicyMode
MaximumRetryCount int
}
type RestartPolicyMode string
const (
RestartPolicyDisabled RestartPolicyMode = "no"
RestartPolicyAlways RestartPolicyMode = "always"
RestartPolicyOnFailure RestartPolicyMode = "on-failure"
RestartPolicyUnlessStopped RestartPolicyMode = "unless-stopped"
)
// IsNone indicates whether the container has the "no" restart policy.
// This means the container will not automatically restart when exiting.
func (rp *RestartPolicy) IsNone() bool {
return rp.Name == "no" || rp.Name == ""
return rp.Name == RestartPolicyDisabled || rp.Name == ""
}
// IsAlways indicates whether the container has the "always" restart policy.
// This means the container will automatically restart regardless of the exit status.
func (rp *RestartPolicy) IsAlways() bool {
return rp.Name == "always"
return rp.Name == RestartPolicyAlways
}
// IsOnFailure indicates whether the container has the "on-failure" restart policy.
// This means the container will automatically restart of exiting with a non-zero exit status.
func (rp *RestartPolicy) IsOnFailure() bool {
return rp.Name == "on-failure"
return rp.Name == RestartPolicyOnFailure
}
// IsUnlessStopped indicates whether the container has the
// "unless-stopped" restart policy. This means the container will
// automatically restart unless user has put it to stopped state.
func (rp *RestartPolicy) IsUnlessStopped() bool {
return rp.Name == "unless-stopped"
return rp.Name == RestartPolicyUnlessStopped
}
// IsSame compares two RestartPolicy to see if they are the same
@@ -305,6 +316,33 @@ func (rp *RestartPolicy) IsSame(tp *RestartPolicy) bool {
return rp.Name == tp.Name && rp.MaximumRetryCount == tp.MaximumRetryCount
}
// ValidateRestartPolicy validates the given RestartPolicy.
func ValidateRestartPolicy(policy RestartPolicy) error {
switch policy.Name {
case RestartPolicyAlways, RestartPolicyUnlessStopped, RestartPolicyDisabled:
if policy.MaximumRetryCount != 0 {
msg := "invalid restart policy: maximum retry count can only be used with 'on-failure'"
if policy.MaximumRetryCount < 0 {
msg += " and cannot be negative"
}
return &errInvalidParameter{fmt.Errorf(msg)}
}
return nil
case RestartPolicyOnFailure:
if policy.MaximumRetryCount < 0 {
return &errInvalidParameter{fmt.Errorf("invalid restart policy: maximum retry count cannot be negative")}
}
return nil
case "":
// Versions before v25.0.0 created an empty restart-policy "name" as
// default. Allow an empty name with "any" MaximumRetryCount for
// backward-compatibility.
return nil
default:
return &errInvalidParameter{fmt.Errorf("invalid restart policy: unknown policy '%s'; use one of '%s', '%s', '%s', or '%s'", policy.Name, RestartPolicyDisabled, RestartPolicyAlways, RestartPolicyOnFailure, RestartPolicyUnlessStopped)}
}
}
// LogMode is a type to define the available modes for logging
// These modes affect how logs are handled when log messages start piling up.
type LogMode string
+8 -7
View File
@@ -1,8 +1,9 @@
//go:build !windows
// +build !windows
package container // import "github.com/docker/docker/api/types/container"
import "github.com/docker/docker/api/types/network"
// IsValid indicates if an isolation technology is valid
func (i Isolation) IsValid() bool {
return i.IsDefault()
@@ -11,15 +12,15 @@ func (i Isolation) IsValid() bool {
// NetworkName returns the name of the network stack.
func (n NetworkMode) NetworkName() string {
if n.IsBridge() {
return "bridge"
return network.NetworkBridge
} else if n.IsHost() {
return "host"
return network.NetworkHost
} else if n.IsContainer() {
return "container"
} else if n.IsNone() {
return "none"
return network.NetworkNone
} else if n.IsDefault() {
return "default"
return network.NetworkDefault
} else if n.IsUserDefined() {
return n.UserDefined()
}
@@ -28,12 +29,12 @@ func (n NetworkMode) NetworkName() string {
// IsBridge indicates whether container uses the bridge network stack
func (n NetworkMode) IsBridge() bool {
return n == "bridge"
return n == network.NetworkBridge
}
// IsHost indicates whether container uses the host network stack.
func (n NetworkMode) IsHost() bool {
return n == "host"
return n == network.NetworkHost
}
// IsUserDefined indicates user-created network
+6 -4
View File
@@ -1,9 +1,11 @@
package container // import "github.com/docker/docker/api/types/container"
import "github.com/docker/docker/api/types/network"
// IsBridge indicates whether container uses the bridge network stack
// in windows it is given the name NAT
func (n NetworkMode) IsBridge() bool {
return n == "nat"
return n == network.NetworkNat
}
// IsHost indicates whether container uses the host network stack.
@@ -25,11 +27,11 @@ func (i Isolation) IsValid() bool {
// NetworkName returns the name of the network stack.
func (n NetworkMode) NetworkName() string {
if n.IsDefault() {
return "default"
return network.NetworkDefault
} else if n.IsBridge() {
return "nat"
return network.NetworkNat
} else if n.IsNone() {
return "none"
return network.NetworkNone
} else if n.IsContainer() {
return "container"
} else if n.IsUserDefined() {
+67
View File
@@ -0,0 +1,67 @@
package container
import "github.com/docker/docker/api/types/filters"
// ResizeOptions holds parameters to resize a TTY.
// It can be used to resize container TTYs and
// exec process TTYs too.
type ResizeOptions struct {
Height uint
Width uint
}
// AttachOptions holds parameters to attach to a container.
type AttachOptions struct {
Stream bool
Stdin bool
Stdout bool
Stderr bool
DetachKeys string
Logs bool
}
// CommitOptions holds parameters to commit changes into a container.
type CommitOptions struct {
Reference string
Comment string
Author string
Changes []string
Pause bool
Config *Config
}
// RemoveOptions holds parameters to remove containers.
type RemoveOptions struct {
RemoveVolumes bool
RemoveLinks bool
Force bool
}
// StartOptions holds parameters to start containers.
type StartOptions struct {
CheckpointID string
CheckpointDir string
}
// ListOptions holds parameters to list containers with.
type ListOptions struct {
Size bool
All bool
Latest bool
Since string
Before string
Limit int
Filters filters.Args
}
// LogsOptions holds parameters to filter logs with.
type LogsOptions struct {
ShowStdout bool
ShowStderr bool
Since string
Until string
Timestamps bool
Follow bool
Tail string
Details bool
}
+82 -2
View File
@@ -1,7 +1,7 @@
package events // import "github.com/docker/docker/api/types/events"
// Type is used for event-types.
type Type = string
type Type string
// List of known event types.
const (
@@ -18,6 +18,86 @@ const (
VolumeEventType Type = "volume" // VolumeEventType is the event type that volumes generate.
)
// Action is used for event-actions.
type Action string
const (
ActionCreate Action = "create"
ActionStart Action = "start"
ActionRestart Action = "restart"
ActionStop Action = "stop"
ActionCheckpoint Action = "checkpoint"
ActionPause Action = "pause"
ActionUnPause Action = "unpause"
ActionAttach Action = "attach"
ActionDetach Action = "detach"
ActionResize Action = "resize"
ActionUpdate Action = "update"
ActionRename Action = "rename"
ActionKill Action = "kill"
ActionDie Action = "die"
ActionOOM Action = "oom"
ActionDestroy Action = "destroy"
ActionRemove Action = "remove"
ActionCommit Action = "commit"
ActionTop Action = "top"
ActionCopy Action = "copy"
ActionArchivePath Action = "archive-path"
ActionExtractToDir Action = "extract-to-dir"
ActionExport Action = "export"
ActionImport Action = "import"
ActionSave Action = "save"
ActionLoad Action = "load"
ActionTag Action = "tag"
ActionUnTag Action = "untag"
ActionPush Action = "push"
ActionPull Action = "pull"
ActionPrune Action = "prune"
ActionDelete Action = "delete"
ActionEnable Action = "enable"
ActionDisable Action = "disable"
ActionConnect Action = "connect"
ActionDisconnect Action = "disconnect"
ActionReload Action = "reload"
ActionMount Action = "mount"
ActionUnmount Action = "unmount"
// ActionExecCreate is the prefix used for exec_create events. These
// event-actions are commonly followed by a colon and space (": "),
// and the command that's defined for the exec, for example:
//
// exec_create: /bin/sh -c 'echo hello'
//
// This is far from ideal; it's a compromise to allow filtering and
// to preserve backward-compatibility.
ActionExecCreate Action = "exec_create"
// ActionExecStart is the prefix used for exec_create events. These
// event-actions are commonly followed by a colon and space (": "),
// and the command that's defined for the exec, for example:
//
// exec_start: /bin/sh -c 'echo hello'
//
// This is far from ideal; it's a compromise to allow filtering and
// to preserve backward-compatibility.
ActionExecStart Action = "exec_start"
ActionExecDie Action = "exec_die"
ActionExecDetach Action = "exec_detach"
// ActionHealthStatus is the prefix to use for health_status events.
//
// Health-status events can either have a pre-defined status, in which
// case the "health_status" action is followed by a colon, or can be
// "free-form", in which case they're followed by the output of the
// health-check output.
//
// This is far form ideal, and a compromise to allow filtering, and
// to preserve backward-compatibility.
ActionHealthStatus Action = "health_status"
ActionHealthStatusRunning Action = "health_status: running"
ActionHealthStatusHealthy Action = "health_status: healthy"
ActionHealthStatusUnhealthy Action = "health_status: unhealthy"
)
// Actor describes something that generates events,
// like a container, or a network, or a volume.
// It has a defined name and a set of attributes.
@@ -37,7 +117,7 @@ type Message struct {
From string `json:"from,omitempty"` // Deprecated: use Actor.Attributes["image"] instead.
Type Type
Action string
Action Action
Actor Actor
// Engine events are local scope. Cluster events are swarm scope.
Scope string `json:"scope,omitempty"`
@@ -1,11 +1,11 @@
package types
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ImageDeleteResponseItem image delete response item
// swagger:model ImageDeleteResponseItem
type ImageDeleteResponseItem struct {
// DeleteResponse delete response
// swagger:model DeleteResponse
type DeleteResponse struct {
// The image ID of an image that was deleted
Deleted string `json:"Deleted,omitempty"`
+9
View File
@@ -0,0 +1,9 @@
package image
import "time"
// Metadata contains engine-local data about the image.
type Metadata struct {
// LastTagTime is the date and time at which the image was last tagged.
LastTagTime time.Time `json:",omitempty"`
}
@@ -1,11 +1,11 @@
package types
package image
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ImageSummary image summary
// swagger:model ImageSummary
type ImageSummary struct {
// Summary summary
// swagger:model Summary
type Summary struct {
// Number of containers using this image. Includes both stopped and running
// containers.
@@ -84,11 +84,6 @@ type ImageSummary struct {
// Total size of the image including all layers it is composed of.
//
// In versions of Docker before v1.10, this field was calculated from
// the image itself and all of its parent images. Images are now stored
// self-contained, and no longer use a parent-chain, making this field
// an equivalent of the Size field.
//
// Deprecated: this field is kept for backward compatibility, and will be removed in API v1.44.
// Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead.
VirtualSize int64 `json:"VirtualSize,omitempty"`
}
+6 -1
View File
@@ -29,7 +29,7 @@ type Mount struct {
// Source is not supported for tmpfs (must be an empty value)
Source string `json:",omitempty"`
Target string `json:",omitempty"`
ReadOnly bool `json:",omitempty"`
ReadOnly bool `json:",omitempty"` // attempts recursive read-only if possible
Consistency Consistency `json:",omitempty"`
BindOptions *BindOptions `json:",omitempty"`
@@ -85,6 +85,11 @@ type BindOptions struct {
Propagation Propagation `json:",omitempty"`
NonRecursive bool `json:",omitempty"`
CreateMountpoint bool `json:",omitempty"`
// ReadOnlyNonRecursive makes the mount non-recursively read-only, but still leaves the mount recursive
// (unless NonRecursive is set to true in conjunction).
ReadOnlyNonRecursive bool `json:",omitempty"`
// ReadOnlyForceRecursive raises an error if the mount cannot be made recursively read-only.
ReadOnlyForceRecursive bool `json:",omitempty"`
}
// VolumeOptions represents the options for a mount of type volume.
+147
View File
@@ -0,0 +1,147 @@
package network
import (
"errors"
"fmt"
"net"
"github.com/docker/docker/internal/multierror"
)
// EndpointSettings stores the network endpoint details
type EndpointSettings struct {
// Configurations
IPAMConfig *EndpointIPAMConfig
Links []string
Aliases []string // Aliases holds the list of extra, user-specified DNS names for this endpoint.
// MacAddress may be used to specify a MAC address when the container is created.
// Once the container is running, it becomes operational data (it may contain a
// generated address).
MacAddress string
// Operational data
NetworkID string
EndpointID string
Gateway string
IPAddress string
IPPrefixLen int
IPv6Gateway string
GlobalIPv6Address string
GlobalIPv6PrefixLen int
DriverOpts map[string]string
// DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to
// generate PTR records.
DNSNames []string
}
// Copy makes a deep copy of `EndpointSettings`
func (es *EndpointSettings) Copy() *EndpointSettings {
epCopy := *es
if es.IPAMConfig != nil {
epCopy.IPAMConfig = es.IPAMConfig.Copy()
}
if es.Links != nil {
links := make([]string, 0, len(es.Links))
epCopy.Links = append(links, es.Links...)
}
if es.Aliases != nil {
aliases := make([]string, 0, len(es.Aliases))
epCopy.Aliases = append(aliases, es.Aliases...)
}
if len(es.DNSNames) > 0 {
epCopy.DNSNames = make([]string, len(es.DNSNames))
copy(epCopy.DNSNames, es.DNSNames)
}
return &epCopy
}
// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
IPv4Address string `json:",omitempty"`
IPv6Address string `json:",omitempty"`
LinkLocalIPs []string `json:",omitempty"`
}
// Copy makes a copy of the endpoint ipam config
func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
cfgCopy := *cfg
cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
return &cfgCopy
}
// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an
// EndpointIPAMConfig is valid for a specific network.
type NetworkSubnet interface {
// Contains checks whether the NetworkSubnet contains [addr].
Contains(addr net.IP) bool
// IsStatic checks whether the subnet was statically allocated (ie. user-defined).
IsStatic() bool
}
// IsInRange checks whether static IP addresses are valid in a specific network.
func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error {
var errs []error
if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil {
errs = append(errs, err)
}
if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil {
errs = append(errs, err)
}
return multierror.Join(errs...)
}
func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error {
if epAddr == "" {
return nil
}
var staticSubnet bool
parsedAddr := net.ParseIP(epAddr)
for _, subnet := range ipamSubnets {
if subnet.IsStatic() {
staticSubnet = true
if subnet.Contains(parsedAddr) {
return nil
}
}
}
if staticSubnet {
return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr)
}
return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets")
}
// Validate checks whether cfg is valid.
func (cfg *EndpointIPAMConfig) Validate() error {
if cfg == nil {
return nil
}
var errs []error
if cfg.IPv4Address != "" {
if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() {
errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address))
}
}
if cfg.IPv6Address != "" {
if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() {
errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address))
}
}
for _, addr := range cfg.LinkLocalIPs {
if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() {
errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr))
}
}
return multierror.Join(errs...)
}
+134
View File
@@ -0,0 +1,134 @@
package network
import (
"errors"
"fmt"
"net/netip"
"github.com/docker/docker/internal/multierror"
)
// IPAM represents IP Address Management
type IPAM struct {
Driver string
Options map[string]string // Per network IPAM driver options
Config []IPAMConfig
}
// IPAMConfig represents IPAM configurations
type IPAMConfig struct {
Subnet string `json:",omitempty"`
IPRange string `json:",omitempty"`
Gateway string `json:",omitempty"`
AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
}
type ipFamily string
const (
ip4 ipFamily = "IPv4"
ip6 ipFamily = "IPv6"
)
// ValidateIPAM checks whether the network's IPAM passed as argument is valid. It returns a joinError of the list of
// errors found.
func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error {
if ipam == nil {
return nil
}
var errs []error
for _, cfg := range ipam.Config {
subnet, err := netip.ParsePrefix(cfg.Subnet)
if err != nil {
errs = append(errs, fmt.Errorf("invalid subnet %s: invalid CIDR block notation", cfg.Subnet))
continue
}
subnetFamily := ip4
if subnet.Addr().Is6() {
subnetFamily = ip6
}
if !enableIPv6 && subnetFamily == ip6 {
continue
}
if subnet != subnet.Masked() {
errs = append(errs, fmt.Errorf("invalid subnet %s: it should be %s", subnet, subnet.Masked()))
}
if ipRangeErrs := validateIPRange(cfg.IPRange, subnet, subnetFamily); len(ipRangeErrs) > 0 {
errs = append(errs, ipRangeErrs...)
}
if err := validateAddress(cfg.Gateway, subnet, subnetFamily); err != nil {
errs = append(errs, fmt.Errorf("invalid gateway %s: %w", cfg.Gateway, err))
}
for auxName, aux := range cfg.AuxAddress {
if err := validateAddress(aux, subnet, subnetFamily); err != nil {
errs = append(errs, fmt.Errorf("invalid auxiliary address %s: %w", auxName, err))
}
}
}
if err := multierror.Join(errs...); err != nil {
return fmt.Errorf("invalid network config:\n%w", err)
}
return nil
}
func validateIPRange(ipRange string, subnet netip.Prefix, subnetFamily ipFamily) []error {
if ipRange == "" {
return nil
}
prefix, err := netip.ParsePrefix(ipRange)
if err != nil {
return []error{fmt.Errorf("invalid ip-range %s: invalid CIDR block notation", ipRange)}
}
family := ip4
if prefix.Addr().Is6() {
family = ip6
}
if family != subnetFamily {
return []error{fmt.Errorf("invalid ip-range %s: parent subnet is an %s block", ipRange, subnetFamily)}
}
var errs []error
if prefix.Bits() < subnet.Bits() {
errs = append(errs, fmt.Errorf("invalid ip-range %s: CIDR block is bigger than its parent subnet %s", ipRange, subnet))
}
if prefix != prefix.Masked() {
errs = append(errs, fmt.Errorf("invalid ip-range %s: it should be %s", prefix, prefix.Masked()))
}
if !subnet.Overlaps(prefix) {
errs = append(errs, fmt.Errorf("invalid ip-range %s: parent subnet %s doesn't contain ip-range", ipRange, subnet))
}
return errs
}
func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) error {
if address == "" {
return nil
}
addr, err := netip.ParseAddr(address)
if err != nil {
return errors.New("invalid address")
}
family := ip4
if addr.Is6() {
family = ip6
}
if family != subnetFamily {
return fmt.Errorf("parent subnet is an %s block", subnetFamily)
}
if !subnet.Contains(addr) {
return fmt.Errorf("parent subnet %s doesn't contain this address", subnet)
}
return nil
}
+14 -68
View File
@@ -1,69 +1,34 @@
package network // import "github.com/docker/docker/api/types/network"
import (
"github.com/docker/docker/api/types/filters"
)
const (
// NetworkDefault is a platform-independent alias to choose the platform-specific default network stack.
NetworkDefault = "default"
// NetworkHost is the name of the predefined network used when the NetworkMode host is selected (only available on Linux)
NetworkHost = "host"
// NetworkNone is the name of the predefined network used when the NetworkMode none is selected (available on both Linux and Windows)
NetworkNone = "none"
// NetworkBridge is the name of the default network on Linux
NetworkBridge = "bridge"
// NetworkNat is the name of the default network on Windows
NetworkNat = "nat"
)
// Address represents an IP address
type Address struct {
Addr string
PrefixLen int
}
// IPAM represents IP Address Management
type IPAM struct {
Driver string
Options map[string]string // Per network IPAM driver options
Config []IPAMConfig
}
// IPAMConfig represents IPAM configurations
type IPAMConfig struct {
Subnet string `json:",omitempty"`
IPRange string `json:",omitempty"`
Gateway string `json:",omitempty"`
AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"`
}
// EndpointIPAMConfig represents IPAM configurations for the endpoint
type EndpointIPAMConfig struct {
IPv4Address string `json:",omitempty"`
IPv6Address string `json:",omitempty"`
LinkLocalIPs []string `json:",omitempty"`
}
// Copy makes a copy of the endpoint ipam config
func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig {
cfgCopy := *cfg
cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs))
cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...)
return &cfgCopy
}
// PeerInfo represents one peer of an overlay network
type PeerInfo struct {
Name string
IP string
}
// EndpointSettings stores the network endpoint details
type EndpointSettings struct {
// Configurations
IPAMConfig *EndpointIPAMConfig
Links []string
Aliases []string
// Operational data
NetworkID string
EndpointID string
Gateway string
IPAddress string
IPPrefixLen int
IPv6Gateway string
GlobalIPv6Address string
GlobalIPv6PrefixLen int
MacAddress string
DriverOpts map[string]string
}
// Task carries the information about one backend task
type Task struct {
Name string
@@ -80,25 +45,6 @@ type ServiceInfo struct {
Tasks []Task
}
// Copy makes a deep copy of `EndpointSettings`
func (es *EndpointSettings) Copy() *EndpointSettings {
epCopy := *es
if es.IPAMConfig != nil {
epCopy.IPAMConfig = es.IPAMConfig.Copy()
}
if es.Links != nil {
links := make([]string, 0, len(es.Links))
epCopy.Links = append(links, es.Links...)
}
if es.Aliases != nil {
aliases := make([]string, 0, len(es.Aliases))
epCopy.Aliases = append(aliases, es.Aliases...)
}
return &epCopy
}
// NetworkingConfig represents the container's networking configuration for each of its interfaces
// Carries the networking configs specified in the `docker run` and `docker network connect` commands
type NetworkingConfig struct {
+3 -1
View File
@@ -92,7 +92,9 @@ type SearchResult struct {
IsOfficial bool `json:"is_official"`
// Name is the name of the repository
Name string `json:"name"`
// IsAutomated indicates whether the result is automated
// IsAutomated indicates whether the result is automated.
//
// Deprecated: the "is_automated" field is deprecated and will always be "false" in the future.
IsAutomated bool `json:"is_automated"`
// Description is a textual description of the repository
Description string `json:"description"`
+41 -2
View File
@@ -32,6 +32,42 @@ type SELinuxContext struct {
Level string
}
// SeccompMode is the type used for the enumeration of possible seccomp modes
// in SeccompOpts
type SeccompMode string
const (
SeccompModeDefault SeccompMode = "default"
SeccompModeUnconfined SeccompMode = "unconfined"
SeccompModeCustom SeccompMode = "custom"
)
// SeccompOpts defines the options for configuring seccomp on a swarm-managed
// container.
type SeccompOpts struct {
// Mode is the SeccompMode used for the container.
Mode SeccompMode `json:",omitempty"`
// Profile is the custom seccomp profile as a json object to be used with
// the container. Mode should be set to SeccompModeCustom when using a
// custom profile in this manner.
Profile []byte `json:",omitempty"`
}
// AppArmorMode is type used for the enumeration of possible AppArmor modes in
// AppArmorOpts
type AppArmorMode string
const (
AppArmorModeDefault AppArmorMode = "default"
AppArmorModeDisabled AppArmorMode = "disabled"
)
// AppArmorOpts defines the options for configuring AppArmor on a swarm-managed
// container. Currently, custom AppArmor profiles are not supported.
type AppArmorOpts struct {
Mode AppArmorMode `json:",omitempty"`
}
// CredentialSpec for managed service account (Windows only)
type CredentialSpec struct {
Config string
@@ -41,8 +77,11 @@ type CredentialSpec struct {
// Privileges defines the security options for the container.
type Privileges struct {
CredentialSpec *CredentialSpec
SELinuxContext *SELinuxContext
CredentialSpec *CredentialSpec
SELinuxContext *SELinuxContext
Seccomp *SeccompOpts `json:",omitempty"`
AppArmor *AppArmorOpts `json:",omitempty"`
NoNewPrivileges bool
}
// ContainerSpec represents the spec of a container.
+1 -1
View File
@@ -1,3 +1,3 @@
//go:generate protoc -I . --gogofast_out=import_path=github.com/docker/docker/api/types/swarm/runtime:. plugin.proto
//go:generate protoc --gogofaster_out=import_path=github.com/docker/docker/api/types/swarm/runtime:. plugin.proto
package runtime // import "github.com/docker/docker/api/types/swarm/runtime"
+227 -173
View File
@@ -1,23 +1,15 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: plugin.proto
/*
Package runtime is a generated protocol buffer package.
It is generated from these files:
plugin.proto
It has these top-level messages:
PluginSpec
PluginPrivilege
*/
package runtime
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import io "io"
import (
fmt "fmt"
proto "github.com/gogo/protobuf/proto"
io "io"
math "math"
math_bits "math/bits"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
@@ -28,22 +20,50 @@ var _ = math.Inf
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
// PluginSpec defines the base payload which clients can specify for creating
// a service with the plugin runtime.
type PluginSpec struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Remote string `protobuf:"bytes,2,opt,name=remote,proto3" json:"remote,omitempty"`
Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges" json:"privileges,omitempty"`
Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges,proto3" json:"privileges,omitempty"`
Disabled bool `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"`
Env []string `protobuf:"bytes,5,rep,name=env" json:"env,omitempty"`
Env []string `protobuf:"bytes,5,rep,name=env,proto3" json:"env,omitempty"`
}
func (m *PluginSpec) Reset() { *m = PluginSpec{} }
func (m *PluginSpec) String() string { return proto.CompactTextString(m) }
func (*PluginSpec) ProtoMessage() {}
func (*PluginSpec) Descriptor() ([]byte, []int) { return fileDescriptorPlugin, []int{0} }
func (m *PluginSpec) Reset() { *m = PluginSpec{} }
func (m *PluginSpec) String() string { return proto.CompactTextString(m) }
func (*PluginSpec) ProtoMessage() {}
func (*PluginSpec) Descriptor() ([]byte, []int) {
return fileDescriptor_22a625af4bc1cc87, []int{0}
}
func (m *PluginSpec) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *PluginSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_PluginSpec.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *PluginSpec) XXX_Merge(src proto.Message) {
xxx_messageInfo_PluginSpec.Merge(m, src)
}
func (m *PluginSpec) XXX_Size() int {
return m.Size()
}
func (m *PluginSpec) XXX_DiscardUnknown() {
xxx_messageInfo_PluginSpec.DiscardUnknown(m)
}
var xxx_messageInfo_PluginSpec proto.InternalMessageInfo
func (m *PluginSpec) GetName() string {
if m != nil {
@@ -85,13 +105,41 @@ func (m *PluginSpec) GetEnv() []string {
type PluginPrivilege struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
Value []string `protobuf:"bytes,3,rep,name=value" json:"value,omitempty"`
Value []string `protobuf:"bytes,3,rep,name=value,proto3" json:"value,omitempty"`
}
func (m *PluginPrivilege) Reset() { *m = PluginPrivilege{} }
func (m *PluginPrivilege) String() string { return proto.CompactTextString(m) }
func (*PluginPrivilege) ProtoMessage() {}
func (*PluginPrivilege) Descriptor() ([]byte, []int) { return fileDescriptorPlugin, []int{1} }
func (m *PluginPrivilege) Reset() { *m = PluginPrivilege{} }
func (m *PluginPrivilege) String() string { return proto.CompactTextString(m) }
func (*PluginPrivilege) ProtoMessage() {}
func (*PluginPrivilege) Descriptor() ([]byte, []int) {
return fileDescriptor_22a625af4bc1cc87, []int{1}
}
func (m *PluginPrivilege) XXX_Unmarshal(b []byte) error {
return m.Unmarshal(b)
}
func (m *PluginPrivilege) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
if deterministic {
return xxx_messageInfo_PluginPrivilege.Marshal(b, m, deterministic)
} else {
b = b[:cap(b)]
n, err := m.MarshalToSizedBuffer(b)
if err != nil {
return nil, err
}
return b[:n], nil
}
}
func (m *PluginPrivilege) XXX_Merge(src proto.Message) {
xxx_messageInfo_PluginPrivilege.Merge(m, src)
}
func (m *PluginPrivilege) XXX_Size() int {
return m.Size()
}
func (m *PluginPrivilege) XXX_DiscardUnknown() {
xxx_messageInfo_PluginPrivilege.DiscardUnknown(m)
}
var xxx_messageInfo_PluginPrivilege proto.InternalMessageInfo
func (m *PluginPrivilege) GetName() string {
if m != nil {
@@ -118,10 +166,32 @@ func init() {
proto.RegisterType((*PluginSpec)(nil), "PluginSpec")
proto.RegisterType((*PluginPrivilege)(nil), "PluginPrivilege")
}
func init() { proto.RegisterFile("plugin.proto", fileDescriptor_22a625af4bc1cc87) }
var fileDescriptor_22a625af4bc1cc87 = []byte{
// 225 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xc8, 0x29, 0x4d,
0xcf, 0xcc, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x9a, 0xc1, 0xc8, 0xc5, 0x15, 0x00, 0x16,
0x08, 0x2e, 0x48, 0x4d, 0x16, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d, 0x95, 0x60, 0x54, 0x60,
0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x85, 0xc4, 0xb8, 0xd8, 0x8a, 0x52, 0x73, 0xf3, 0x4b, 0x52, 0x25,
0x98, 0xc0, 0xa2, 0x50, 0x9e, 0x90, 0x01, 0x17, 0x57, 0x41, 0x51, 0x66, 0x59, 0x66, 0x4e, 0x6a,
0x7a, 0x6a, 0xb1, 0x04, 0xb3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x80, 0x1e, 0xc4, 0xb0, 0x00, 0x98,
0x44, 0x10, 0x92, 0x1a, 0x21, 0x29, 0x2e, 0x8e, 0x94, 0xcc, 0xe2, 0xc4, 0xa4, 0x9c, 0xd4, 0x14,
0x09, 0x16, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0x39, 0x35, 0xaf, 0x4c,
0x82, 0x55, 0x81, 0x59, 0x83, 0x33, 0x08, 0xc4, 0x54, 0x8a, 0xe5, 0xe2, 0x47, 0x33, 0x0c, 0xab,
0xf3, 0x14, 0xb8, 0xb8, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, 0x0b, 0x4a, 0x32, 0xf3, 0xf3, 0xa0,
0x6e, 0x44, 0x16, 0x12, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x05, 0xbb, 0x91, 0x33,
0x08, 0xc2, 0x71, 0x92, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4,
0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x24, 0x36,
0x70, 0xd0, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x37, 0xea, 0xe2, 0xca, 0x2a, 0x01, 0x00,
0x00,
}
func (m *PluginSpec) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -129,66 +199,69 @@ func (m *PluginSpec) Marshal() (dAtA []byte, err error) {
}
func (m *PluginSpec) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *PluginSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Remote) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Remote)))
i += copy(dAtA[i:], m.Remote)
}
if len(m.Privileges) > 0 {
for _, msg := range m.Privileges {
dAtA[i] = 0x1a
i++
i = encodeVarintPlugin(dAtA, i, uint64(msg.Size()))
n, err := msg.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n
if len(m.Env) > 0 {
for iNdEx := len(m.Env) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Env[iNdEx])
copy(dAtA[i:], m.Env[iNdEx])
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Env[iNdEx])))
i--
dAtA[i] = 0x2a
}
}
if m.Disabled {
dAtA[i] = 0x20
i++
i--
if m.Disabled {
dAtA[i] = 1
} else {
dAtA[i] = 0
}
i++
i--
dAtA[i] = 0x20
}
if len(m.Env) > 0 {
for _, s := range m.Env {
dAtA[i] = 0x2a
i++
l = len(s)
for l >= 1<<7 {
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
l >>= 7
i++
if len(m.Privileges) > 0 {
for iNdEx := len(m.Privileges) - 1; iNdEx >= 0; iNdEx-- {
{
size, err := m.Privileges[iNdEx].MarshalToSizedBuffer(dAtA[:i])
if err != nil {
return 0, err
}
i -= size
i = encodeVarintPlugin(dAtA, i, uint64(size))
}
dAtA[i] = uint8(l)
i++
i += copy(dAtA[i:], s)
i--
dAtA[i] = 0x1a
}
}
return i, nil
if len(m.Remote) > 0 {
i -= len(m.Remote)
copy(dAtA[i:], m.Remote)
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Remote)))
i--
dAtA[i] = 0x12
}
if len(m.Name) > 0 {
i -= len(m.Name)
copy(dAtA[i:], m.Name)
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func (m *PluginPrivilege) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
n, err := m.MarshalToSizedBuffer(dAtA[:size])
if err != nil {
return nil, err
}
@@ -196,50 +269,56 @@ func (m *PluginPrivilege) Marshal() (dAtA []byte, err error) {
}
func (m *PluginPrivilege) MarshalTo(dAtA []byte) (int, error) {
var i int
size := m.Size()
return m.MarshalToSizedBuffer(dAtA[:size])
}
func (m *PluginPrivilege) MarshalToSizedBuffer(dAtA []byte) (int, error) {
i := len(dAtA)
_ = i
var l int
_ = l
if len(m.Name) > 0 {
dAtA[i] = 0xa
i++
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
i += copy(dAtA[i:], m.Name)
}
if len(m.Description) > 0 {
dAtA[i] = 0x12
i++
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Description)))
i += copy(dAtA[i:], m.Description)
}
if len(m.Value) > 0 {
for _, s := range m.Value {
for iNdEx := len(m.Value) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Value[iNdEx])
copy(dAtA[i:], m.Value[iNdEx])
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Value[iNdEx])))
i--
dAtA[i] = 0x1a
i++
l = len(s)
for l >= 1<<7 {
dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
l >>= 7
i++
}
dAtA[i] = uint8(l)
i++
i += copy(dAtA[i:], s)
}
}
return i, nil
if len(m.Description) > 0 {
i -= len(m.Description)
copy(dAtA[i:], m.Description)
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Description)))
i--
dAtA[i] = 0x12
}
if len(m.Name) > 0 {
i -= len(m.Name)
copy(dAtA[i:], m.Name)
i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name)))
i--
dAtA[i] = 0xa
}
return len(dAtA) - i, nil
}
func encodeVarintPlugin(dAtA []byte, offset int, v uint64) int {
offset -= sovPlugin(v)
base := offset
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
return base
}
func (m *PluginSpec) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Name)
@@ -269,6 +348,9 @@ func (m *PluginSpec) Size() (n int) {
}
func (m *PluginPrivilege) Size() (n int) {
if m == nil {
return 0
}
var l int
_ = l
l = len(m.Name)
@@ -289,14 +371,7 @@ func (m *PluginPrivilege) Size() (n int) {
}
func sovPlugin(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
return (math_bits.Len64(x|1) + 6) / 7
}
func sozPlugin(x uint64) (n int) {
return sovPlugin(uint64((x << 1) ^ uint64((int64(x) >> 63))))
@@ -316,7 +391,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -344,7 +419,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -354,6 +429,9 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -373,7 +451,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -383,6 +461,9 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -402,7 +483,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
msglen |= int(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -411,6 +492,9 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + msglen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -433,7 +517,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
v |= (int(b) & 0x7F) << shift
v |= int(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -453,7 +537,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -463,6 +547,9 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -474,7 +561,7 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthPlugin
}
if (iNdEx + skippy) > l {
@@ -504,7 +591,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
wire |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -532,7 +619,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -542,6 +629,9 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -561,7 +651,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -571,6 +661,9 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -590,7 +683,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
@@ -600,6 +693,9 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
return ErrInvalidLengthPlugin
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLengthPlugin
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
@@ -611,7 +707,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
if err != nil {
return err
}
if skippy < 0 {
if (skippy < 0) || (iNdEx+skippy) < 0 {
return ErrInvalidLengthPlugin
}
if (iNdEx + skippy) > l {
@@ -629,6 +725,7 @@ func (m *PluginPrivilege) Unmarshal(dAtA []byte) error {
func skipPlugin(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
depth := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
@@ -660,10 +757,8 @@ func skipPlugin(dAtA []byte) (n int, err error) {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
@@ -680,75 +775,34 @@ func skipPlugin(dAtA []byte) (n int, err error) {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthPlugin
}
return iNdEx, nil
iNdEx += length
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowPlugin
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipPlugin(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
depth++
case 4:
return iNdEx, nil
if depth == 0 {
return 0, ErrUnexpectedEndOfGroupPlugin
}
depth--
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
if iNdEx < 0 {
return 0, ErrInvalidLengthPlugin
}
if depth == 0 {
return iNdEx, nil
}
}
panic("unreachable")
return 0, io.ErrUnexpectedEOF
}
var (
ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowPlugin = fmt.Errorf("proto: integer overflow")
ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowPlugin = fmt.Errorf("proto: integer overflow")
ErrUnexpectedEndOfGroupPlugin = fmt.Errorf("proto: unexpected end of group")
)
func init() { proto.RegisterFile("plugin.proto", fileDescriptorPlugin) }
var fileDescriptorPlugin = []byte{
// 256 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x4d, 0x4b, 0xc3, 0x30,
0x18, 0xc7, 0x89, 0xdd, 0xc6, 0xfa, 0x4c, 0x70, 0x04, 0x91, 0xe2, 0xa1, 0x94, 0x9d, 0x7a, 0x6a,
0x45, 0x2f, 0x82, 0x37, 0x0f, 0x9e, 0x47, 0xbc, 0x09, 0x1e, 0xd2, 0xf6, 0xa1, 0x06, 0x9b, 0x17,
0x92, 0xb4, 0xe2, 0x37, 0xf1, 0x23, 0x79, 0xf4, 0x23, 0x48, 0x3f, 0x89, 0x98, 0x75, 0x32, 0x64,
0xa7, 0xff, 0x4b, 0xc2, 0x9f, 0x1f, 0x0f, 0x9c, 0x9a, 0xae, 0x6f, 0x85, 0x2a, 0x8c, 0xd5, 0x5e,
0x6f, 0x3e, 0x08, 0xc0, 0x36, 0x14, 0x8f, 0x06, 0x6b, 0x4a, 0x61, 0xa6, 0xb8, 0xc4, 0x84, 0x64,
0x24, 0x8f, 0x59, 0xf0, 0xf4, 0x02, 0x16, 0x16, 0xa5, 0xf6, 0x98, 0x9c, 0x84, 0x76, 0x4a, 0xf4,
0x0a, 0xc0, 0x58, 0x31, 0x88, 0x0e, 0x5b, 0x74, 0x49, 0x94, 0x45, 0xf9, 0xea, 0x7a, 0x5d, 0xec,
0xc6, 0xb6, 0xfb, 0x07, 0x76, 0xf0, 0x87, 0x5e, 0xc2, 0xb2, 0x11, 0x8e, 0x57, 0x1d, 0x36, 0xc9,
0x2c, 0x23, 0xf9, 0x92, 0xfd, 0x65, 0xba, 0x86, 0x08, 0xd5, 0x90, 0xcc, 0xb3, 0x28, 0x8f, 0xd9,
0xaf, 0xdd, 0x3c, 0xc3, 0xd9, 0xbf, 0xb1, 0xa3, 0x78, 0x19, 0xac, 0x1a, 0x74, 0xb5, 0x15, 0xc6,
0x0b, 0xad, 0x26, 0xc6, 0xc3, 0x8a, 0x9e, 0xc3, 0x7c, 0xe0, 0x5d, 0x8f, 0x81, 0x31, 0x66, 0xbb,
0x70, 0xff, 0xf0, 0x39, 0xa6, 0xe4, 0x6b, 0x4c, 0xc9, 0xf7, 0x98, 0x92, 0xa7, 0xdb, 0x56, 0xf8,
0x97, 0xbe, 0x2a, 0x6a, 0x2d, 0xcb, 0x46, 0xd7, 0xaf, 0x68, 0xf7, 0xc2, 0x8d, 0x28, 0xfd, 0xbb,
0x41, 0x57, 0xba, 0x37, 0x6e, 0x65, 0x69, 0x7b, 0xe5, 0x85, 0xc4, 0xbb, 0x49, 0xab, 0x45, 0x38,
0xe4, 0xcd, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0xa8, 0xd9, 0x9b, 0x58, 0x01, 0x00, 0x00,
}
-2
View File
@@ -1,7 +1,5 @@
syntax = "proto3";
option go_package = "github.com/docker/docker/api/types/swarm/runtime;runtime";
// PluginSpec defines the base payload which clients can specify for creating
// a service with the plugin runtime.
message PluginSpec {
+3 -3
View File
@@ -34,9 +34,9 @@ type ServiceSpec struct {
UpdateConfig *UpdateConfig `json:",omitempty"`
RollbackConfig *UpdateConfig `json:",omitempty"`
// Networks field in ServiceSpec is deprecated. The
// same field in TaskSpec should be used instead.
// This field will be removed in a future release.
// Networks specifies which networks the service should attach to.
//
// Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead.
Networks []NetworkAttachmentConfig `json:",omitempty"`
EndpointSpec *EndpointSpec `json:",omitempty"`
}
@@ -0,0 +1,20 @@
package swarm
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
// ServiceCreateResponse contains the information returned to a client on the
// creation of a new service.
//
// swagger:model ServiceCreateResponse
type ServiceCreateResponse struct {
// The ID of the created service.
ID string `json:"ID,omitempty"`
// Optional warning message.
//
// FIXME(thaJeztah): this should have "omitempty" in the generated type.
//
Warnings []string `json:"Warnings"`
}
@@ -1,4 +1,4 @@
package types
package swarm
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
+116
View File
@@ -0,0 +1,116 @@
package system
import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
)
// Info contains response of Engine API:
// GET "/info"
type Info struct {
ID string
Containers int
ContainersRunning int
ContainersPaused int
ContainersStopped int
Images int
Driver string
DriverStatus [][2]string
SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API
Plugins PluginsInfo
MemoryLimit bool
SwapLimit bool
KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes
KernelMemoryTCP bool `json:",omitempty"` // KernelMemoryTCP is not supported on cgroups v2.
CPUCfsPeriod bool `json:"CpuCfsPeriod"`
CPUCfsQuota bool `json:"CpuCfsQuota"`
CPUShares bool
CPUSet bool
PidsLimit bool
IPv4Forwarding bool
BridgeNfIptables bool
BridgeNfIP6tables bool `json:"BridgeNfIp6tables"`
Debug bool
NFd int
OomKillDisable bool
NGoroutines int
SystemTime string
LoggingDriver string
CgroupDriver string
CgroupVersion string `json:",omitempty"`
NEventsListener int
KernelVersion string
OperatingSystem string
OSVersion string
OSType string
Architecture string
IndexServerAddress string
RegistryConfig *registry.ServiceConfig
NCPU int
MemTotal int64
GenericResources []swarm.GenericResource
DockerRootDir string
HTTPProxy string `json:"HttpProxy"`
HTTPSProxy string `json:"HttpsProxy"`
NoProxy string
Name string
Labels []string
ExperimentalBuild bool
ServerVersion string
Runtimes map[string]RuntimeWithStatus
DefaultRuntime string
Swarm swarm.Info
// LiveRestoreEnabled determines whether containers should be kept
// running when the daemon is shutdown or upon daemon start if
// running containers are detected
LiveRestoreEnabled bool
Isolation container.Isolation
InitBinary string
ContainerdCommit Commit
RuncCommit Commit
InitCommit Commit
SecurityOptions []string
ProductLicense string `json:",omitempty"`
DefaultAddressPools []NetworkAddressPool `json:",omitempty"`
CDISpecDirs []string
// Legacy API fields for older API versions.
legacyFields
// Warnings contains a slice of warnings that occurred while collecting
// system information. These warnings are intended to be informational
// messages for the user, and are not intended to be parsed / used for
// other purposes, as they do not have a fixed format.
Warnings []string
}
type legacyFields struct {
ExecutionDriver string `json:",omitempty"` // Deprecated: deprecated since API v1.25, but returned for older versions.
}
// PluginsInfo is a temp struct holding Plugins name
// registered with docker daemon. It is used by [Info] struct
type PluginsInfo struct {
// List of Volume plugins registered
Volume []string
// List of Network plugins registered
Network []string
// List of Authorization plugins registered
Authorization []string
// List of Log plugins registered
Log []string
}
// Commit holds the Git-commit (SHA1) that a binary was built from, as reported
// in the version-string of external tools, such as containerd, or runC.
type Commit struct {
ID string // ID is the actual commit ID of external tool.
Expected string // Expected is the commit ID of external tool expected by dockerd as set at build time.
}
// NetworkAddressPool is a temp struct used by [Info] struct.
type NetworkAddressPool struct {
Base string
Size int
}
+20
View File
@@ -0,0 +1,20 @@
package system
// Runtime describes an OCI runtime
type Runtime struct {
// "Legacy" runtime configuration for runc-compatible runtimes.
Path string `json:"path,omitempty"`
Args []string `json:"runtimeArgs,omitempty"`
// Shimv2 runtime configuration. Mutually exclusive with the legacy config above.
Type string `json:"runtimeType,omitempty"`
Options map[string]interface{} `json:"options,omitempty"`
}
// RuntimeWithStatus extends [Runtime] to hold [RuntimeStatus].
type RuntimeWithStatus struct {
Runtime
Status map[string]string `json:"status,omitempty"`
}
+48
View File
@@ -0,0 +1,48 @@
package system
import (
"errors"
"fmt"
"strings"
)
// SecurityOpt contains the name and options of a security option
type SecurityOpt struct {
Name string
Options []KeyValue
}
// DecodeSecurityOptions decodes a security options string slice to a
// type-safe [SecurityOpt].
func DecodeSecurityOptions(opts []string) ([]SecurityOpt, error) {
so := []SecurityOpt{}
for _, opt := range opts {
// support output from a < 1.13 docker daemon
if !strings.Contains(opt, "=") {
so = append(so, SecurityOpt{Name: opt})
continue
}
secopt := SecurityOpt{}
for _, s := range strings.Split(opt, ",") {
k, v, ok := strings.Cut(s, "=")
if !ok {
return nil, fmt.Errorf("invalid security option %q", s)
}
if k == "" || v == "" {
return nil, errors.New("invalid empty security option")
}
if k == "name" {
secopt.Name = v
continue
}
secopt.Options = append(secopt.Options, KeyValue{Key: k, Value: v})
}
so = append(so, secopt)
}
return so, nil
}
// KeyValue holds a key/value pair.
type KeyValue struct {
Key, Value string
}
Loaded 100 of 613 files, more files were not shown because too many files have changed in this diff. Show more