Commit d719243
authored
feat: initial Kilo cluster-mesh operator POC (#2)
* fix(generate): align manifests target with repo layout
Restrict CRD generation to ./api/... so manifests no longer produces
config/crd/bases/kilo.squat.ai_peers.yaml for the external Kilo Peer
type. Regenerate deepcopy files so the boilerplate header from
hack/boilerplate.go.txt is committed, matching what controller-gen
produces in CI.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(lint): exclude goconst from test files
Test files contain CIDRs, namespace names and similar fixtures repeated
across cases; promoting them to constants only obscures intent. Add
goconst to the linter exclusion list for _test\.go.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(chart): drop unsupported --namespace and add --metrics-secure flag
The operator binary does not define a --namespace flag; passing it
caused the manager to exit at startup. Remove the argument from the
Deployment and the corresponding unit test.
The chart wired --metrics-bind-address=:8080 while the binary defaults
metrics-secure to true, which started HTTPS on an HTTP port and made
metrics unscrapeable without a TLS setup. Expose metricsSecure in
values.yaml (default false) and pass --metrics-secure explicitly.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(ci): pass --verify=false to helm plugin install
Helm 4.1 requires plugin source verification by default, which the
helm-unittest source does not support. Without the flag the helm CI
job aborts with "plugin source does not support verification" before
helm lint and unit tests can run.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(ci): publish container image to ghcr.io on push to main
Add an image job that builds the multi-arch Containerfile and pushes
to ghcr.io/cozystack/kilo-clustermesh-operator with :main and
:sha-<commit> tags. The job runs only on push to main and waits for
all checks (lint, test, integration, build, helm, generate) so a
broken commit cannot publish an image. Tagged releases are still
handled by .github/workflows/release.yml.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(lint): extract Kilo group name to a constant
golangci-lint v2.12 flagged "kilo.squat.ai" repeated across register.go
and types_test.go via goconst. Define a GroupName constant in the Kilo
v1alpha1 package and use it from both call sites; this also matches
the convention used by upstream Kubernetes API packages.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(lint): extract Kilo group version to a constant
golangci-lint v2.12 picked up another goconst occurrence:
the literal "v1alpha1" repeated across register.go and types_test.go.
Define a GroupVersion constant alongside GroupName and use it from
both call sites.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* ci: run CI and publish image on the dev branch
Add dev to the push/pull_request trigger lists and to the image job's
allow-list. Switch the image tag source from a fixed raw "main" to
type=ref,event=branch so pushes to dev publish :dev and pushes to
main publish :main. VERSION build-arg follows the ref name.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* perf(container): build Go binary on the native runner platform
Add --platform=\$BUILDPLATFORM to the builder stage so the Go
toolchain runs natively on the GitHub runner (amd64) while
cross-compiling for TARGETARCH through GOOS/GOARCH. QEMU emulation
is now only used for the final distroless stage, which is just a
file copy. ARM64 image builds drop from QEMU-emulated compilation
to native compile + cross-link.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(operator): wire startup bootstrap and migrate to events API
The previous cmd/main.go constructed the manager with an unset Registry,
Log and Recorder on ClusterMeshReconciler, which would have caused a
nil pointer panic on the first reconcile. CRD self-install, despite
being available in internal/crd, was never invoked, so the manager
also failed to start with "no matches for kind ClusterMesh".
Startup now:
- runs crd.InstallOrUpdate before manager creation
- reads its namespace from POD_NAMESPACE (downward API)
- lists every ClusterMesh in that namespace, merges cluster entries by
name and builds a multicluster.ClusterRegistry up-front
- registers each cluster.Cluster with the manager so caches start
alongside the leader
- wires the reconciler with the real Registry, slog logger and the
manager's event recorder
- installs a change-watcher that cancels the manager context when
cluster fingerprints drift, triggering a kubelet-driven restart
The reconciler's Recorder field and the integration test suite are
moved off the deprecated record.EventRecorder API onto the events
package, fixing the staticcheck SA1019 warning.
go mod tidy removes seven now-unused indirect dependencies.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(chart): allow SA token automount, inject POD_NAMESPACE and set seccomp RuntimeDefault
Three blocking issues for running the operator on a cluster with
PodSecurity restricted enforced:
- ServiceAccount.automountServiceAccountToken: false stopped the
controller from authenticating to the in-cluster API. Removed; the
operator needs the projected token.
- The container template did not request POD_NAMESPACE, which the new
startup bootstrap reads to scope its ClusterMesh and Secret lookups.
Injected via downward API.
- securityContext.seccompProfile was unset, triggering a PodSecurity
warning under restricted:latest. Set to RuntimeDefault.
helm-unittest suites are updated accordingly. All 41 tests pass.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(operator): scope manager cache to operator namespace
The controller-runtime manager cache defaulted to cluster-scope list/watch
on ClusterMesh and Secret, which required cluster-wide RBAC the chart
deliberately does not grant. The operator only watches namespaced
resources in its own namespace; cluster-scoped resources (Peers, Nodes,
CRDs, Leases) are accessed via the multicluster registry or direct API
calls and are unaffected.
Restrict Cache.DefaultNamespaces to the operator's own namespace so the
existing namespace-scoped Role is sufficient.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(operator): accept cozystack-Kilo wireguard-ip annotation format
Upstream Kilo writes the kilo.squat.ai/wireguard-ip annotation as a host
route ("10.4.0.1/32"); the cozystack-patched Kilo with cross granularity
writes the host IP with the wireguard subnet mask ("100.66.0.3/16"). The
operator previously required /32 (or /128) and reused the raw annotation
value in the Peer AllowedIPs — both of which fail under cozystack-Kilo:
validation skips every node, and even if it did not, every peer would
claim the entire wireguard subnet.
Add a netutil helper that parses an annotation preserving host bits, and
another that emits the host route for a given IP. Validation now checks
only that the host IP falls inside the cluster's wireguardCIDR. The peer
builder normalises AllowedIPs to a /32 (resp. /128) host route so each
peer terminates traffic for exactly one WireGuard IP.
Covered by unit tests for both upstream-style and cozystack-style
annotations.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(chart): grant operator RBAC on events.k8s.io events
The migration to k8s.io/client-go/tools/events.EventRecorder caused
events to be written to the events.k8s.io/v1 API group instead of the
legacy core "events" resource. The Role only granted access to the
latter, so every recorded event was rejected by the apiserver with
events.events.k8s.io is forbidden.
Grant create/patch on events.k8s.io/events alongside the existing rule
for the core group; keep both so the operator stays compatible if any
component falls back to the legacy API.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(readme): document force-endpoint annotation, manual reconcile and roadmap
The operator only sets a Peer endpoint from kilo.squat.ai/force-endpoint;
clusters where nodes are not publicly reachable on that annotation will
end up with peers that have no endpoint and silently drop traffic. The
manager cache also does not watch Node objects, so annotation changes
require a no-op write on the ClusterMesh resource to trigger a reconcile.
Capture both of these in the README so users hit fewer surprises.
Also refresh the surrounding documentation:
- tighten Prerequisites to distinguish apiserver reachability from
per-node UDP reachability;
- replace the broken Remote Cluster Setup snippet with the working
Secret-backed long-lived token procedure that survives 1.24+;
- correct the Architecture section, which previously claimed the
controller watches Node objects;
- add a Possible Improvements list covering a dedicated cross-cluster
endpoint annotation, a Node watcher, and an explicit per-node skip
annotation.
Stop tracking local cluster-specific deployment manifests under deploy/
by adding it to .gitignore; the upstream README now contains the
generic procedure, so the per-cluster files belong in a private branch.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(kilonode): document both accepted wireguard-ip annotation formats
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(restart): guard against nil Cancel in ChangeWatcher.Reconcile
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(main): add unit tests for mergeClusterSpecs deduplication
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(containerfile): correct image.source label to cozystack repo
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(peer): cover IPv6 and DNS endpoint parsing
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(main): declare version/revision build vars and log at startup
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(chart): default image.repository to cozystack fork
The previous default pointed at the upstream squat image, causing
helm install without overrides to pull the wrong image. Switch the
default to the cozystack fork, add a helm-unittest assertion that
verifies the default, and remove the now-redundant repository override
from the README tag-pinning example.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(validation): normalize wireguard-ip before duplicate detection
Two nodes with "10.4.0.1/16" and "10.4.0.1/32" resolve to the same
WireGuard peer (AllowedIPs = 10.4.0.1/32) and therefore conflict.
The old comparison was a raw-string equality check, so it missed this
case.
Extract the host IP via netutil.ParseHostInCIDR and use that as the
dedup key. Invalid annotations fall back to raw-string keying so that
identical-invalid values still deduplicate without colliding with any
valid IP.
Add three new test cases that cover: same host IP / different prefix
lengths, different host IPs (sanity), and invalid vs valid annotation.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* ci(workflows): include cmd package in unit test job
TestMergeClusterSpecs in cmd/main_test.go was never executed in CI
because the test step listed explicit package paths that omitted
./cmd/.... Add ./cmd/... to the go test invocation so all unit tests
run on every push.
Add internal/citest/workflow_test.go to structurally assert the
presence of ./cmd/... in the ci.yml test step, preventing future
accidental regressions.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* fix(peer): strip brackets from DNS fallback in endpoint parser
buildDNSOrIP strips brackets from the host before calling net.ParseIP,
but the DNS fallback path was returning the unstripped host variable.
A bracketed DNS name like [dns.example.com]:51820 would produce
DNS: "[dns.example.com]", which is an invalid hostname.
Change DNS: host to DNS: cleanHost so the brackets-stripped form is
always used for the DNS field.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(api): correct WireguardCIDR comment about accepted annotation formats
The previous comment stated that wireguard-ip annotations must be a /32
(or /128) host route. This was accurate before the IsHostRoute validation
check was dropped, but has been incorrect since that change. The operator
now accepts any prefix length and validates only the host portion of the
address against WireguardCIDR. Update the Go doc comment and the
generated CRD YAML description to match actual behavior.
Also annotate the existing regression test case to document that it
guards against inadvertent reintroduction of the /32-only requirement.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(validation): cover ValidateClusterNetworks and ValidateMeshNetworks
Add table-driven unit tests for both public functions in
internal/validation/mesh.go, which previously had 0% coverage in CI
(integration tests were excluded from the unit test job).
TestValidateClusterNetworks covers:
- empty cluster list (nil error)
- single cluster, multiple disjoint CIDRs (nil error)
- single cluster with serviceCIDR overlapping additionalCIDR (error)
- two clusters with disjoint CIDRs (nil error)
- two clusters with overlapping serviceCIDR (error naming both clusters)
- two clusters with overlapping podCIDR (error naming both clusters)
- invalid CIDR string triggering a parse error
TestValidateMeshNetworks covers:
- empty mesh list (nil error)
- single mesh with valid clusters (nil error)
- two meshes with disjoint network plans (nil error)
- two meshes with an overlapping CIDR (error naming both meshes)
- intra-mesh overlap detected before cross-mesh check
Helpers makeCluster and makeMesh follow the makeNode convention from
node_test.go.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(readme): use non-overlapping CIDRs in Quick Start example
The original example set identical serviceCIDR (10.96.0.0/12) on both
clusters, and both wireguardCIDRs (10.100.x.0/24) fell inside that /12
range — all three overlaps would have caused ValidateClusterNetworks to
return an error and suppress all Peer creation.
Fix by assigning distinct, non-overlapping ranges to every field:
- cluster-a wireguardCIDR: 10.200.0.0/24
- cluster-b wireguardCIDR: 10.200.1.0/24
- cluster-b serviceCIDR: 10.112.0.0/12
Add TestREADMEQuickStartManifestIsValid in internal/validation/mesh_test.go
as a regression guard: it reads README.md, extracts the ClusterMesh YAML
block, unmarshals it, and asserts ValidateClusterNetworks returns nil.
The test fails with the original CIDRs and passes after this fix.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(peer): clarify allowedIPs derivation in BuildPeer comment
The previous comment said "wireguard-ip annotation" without explaining
that only the host IP is extracted and then normalised to a /32 (IPv4)
or /128 (IPv6) host route. The updated comment makes the normalisation
step explicit, which is important context given that the annotation may
carry a wider subnet mask in cozystack-patched Kilo.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(api): add wireguardPort field to ClusterEntry
Introduces an optional WireguardPort field on ClusterEntry, defaulting to
51820. The operator uses this port when synthesising a peer endpoint from
Node.Status.Addresses (i.e. neither clustermesh-endpoint nor force-endpoint
annotation is set on the node).
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(annotations): add clustermesh-endpoint node annotation
Adds AnnotationClustermeshEndpoint constant for an operator-specific
node annotation that conveys the cross-cluster WireGuard endpoint
independently of Kilo's own kilo.squat.ai/force-endpoint. Decoupling
the two prevents the operator's endpoint configuration from affecting
intra-cluster Kilo behaviour (notably the "cross" mesh granularity).
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(kilonode): add tests for endpoint resolution chain (Red)
12 tests covering ResolveEndpoint priority order, ExternalIP fallback
(IPv4/IPv6 preference, default port), strict error on malformed annotations,
and ignoring non-ExternalIP address types.
Tests intentionally fail to compile until ResolveEndpoint is implemented.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(kilonode): resolve node endpoint via fallback chain
Implements ResolveEndpoint which determines a node's WireGuard endpoint
via a three-tier priority chain: the operator-specific
kilo.squat.ai/clustermesh-endpoint annotation wins; otherwise the legacy
kilo.squat.ai/force-endpoint annotation; otherwise the first ExternalIP
in Node.Status.Addresses (IPv4 preferred over IPv6) combined with the
fallback port.
A present-but-malformed annotation is a hard error rather than a silent
fall-through, so misconfiguration surfaces immediately instead of
yielding peers without an endpoint.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* refactor(peer): pass ClusterEntry through Build{Peer,AnchorPeer}
Replaces the (meshName, sourceCluster string) and
(meshName, sourceCluster, entry) signatures with a unified
(meshName string, entry *ClusterEntry) shape. The entry carries both
the cluster name and the new WireguardPort field, removing the need to
plumb individual fields through call sites and preparing the builders
to consume kilonode.ResolveEndpoint in the next step.
Existing behaviour is preserved; tests pass without modification beyond
the signature change.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(peer): wire endpoint resolution chain into peer builders
BuildPeer and BuildAnchorPeer now consume kilonode.ResolveEndpoint
instead of reading kilo.squat.ai/force-endpoint directly. The peer
endpoint is therefore picked up from the clustermesh-endpoint
annotation, the legacy force-endpoint annotation, or Node.Status
ExternalIPs in that priority order.
Two behaviour changes flow from this:
- A node with no endpoint source no longer produces a Peer without an
endpoint silently — BuildPeer now returns an error and
BuildAnchorPeer returns nil. Validation should already filter such
nodes; the new behaviour is a defensive surface for missed cases.
- A present-but-malformed endpoint annotation (clustermesh-endpoint or
force-endpoint) is a hard error, not a silent fall-through. The
affected unit test was rewritten accordingly.
baseAnnotations() in builder_test.go now includes a valid force-endpoint
so existing tests succeed by default; tests that exercise specific
chain layers override or delete the relevant key.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(validation): add endpoint resolution skip cases (Red)
Adds four ValidateNode test cases that the current implementation does
not satisfy yet: a node missing every endpoint source must be skipped
with ReasonNoEndpoint, a malformed clustermesh-endpoint or
force-endpoint annotation must be skipped with ReasonEndpointInvalid,
and a node whose only endpoint source is an ExternalIP must be accepted.
The pre-existing baseAnnotations() now includes a valid force-endpoint
so that all current "valid node" cases continue to pass once endpoint
validation lands.
Tests fail at compile time because ReasonNoEndpoint and
ReasonEndpointInvalid do not exist yet; the Green commit will add them
alongside the validateEndpoint helper.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* feat(validation): require resolvable endpoint via kilonode.ResolveEndpoint
ValidateNode now exercises the endpoint fallback chain after the
WireGuard IP and public key checks. A node with no source is skipped
with ReasonNoEndpoint; a node whose clustermesh-endpoint or
force-endpoint annotation is present but malformed is skipped with
ReasonEndpointInvalid.
Filtering out such nodes here prevents BuildPeer from later failing
on a per-node basis and surfaces the misconfiguration as a clear
SkippedNodes entry in ClusterMesh status.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* chore(crd): regenerate manifests after wireguardPort addition
Regenerates the ClusterMesh CRD via controller-gen to expose the new
wireguardPort field with default 51820 and bounds [1, 65535], and
mirrors the result into the embedded copy at internal/crd/ which the
operator applies on startup.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(readme): document endpoint resolution chain
Updates the per-node configuration and reconciliation-flow sections to
describe the new three-tier endpoint chain
(kilo.squat.ai/clustermesh-endpoint → kilo.squat.ai/force-endpoint →
first ExternalIP), the strict treatment of malformed annotations
(NodeEndpointInvalid skip reason), and the per-cluster wireguardPort
field used by the ExternalIP fallback.
Removes the now-implemented "dedicated cross-cluster endpoint
annotation" item from the roadmap and updates the prerequisites note
about the WireGuard UDP port to reflect that it is configurable.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs: rewrite README and add docs/ tree
Restructure README as a landing page (overview, requirements, quick
start) and split deep content into a flat English-only docs/ tree:
- docs/architecture.md — components, reconciliation flow, anchor
peer, manager cache scoping, CRD bootstrap,
restart watcher
- docs/installation.md — Helm install, embedded CRD bootstrap,
remote-cluster kubeconfig setup, RBAC,
verification, uninstall
- docs/configuration.md — ClusterMesh CRD reference: Spec, Status,
ClusterEntry fields, conditions, CIDR
validation rules
- docs/per-node-setup.md — required Node annotations, three-tier
endpoint resolution chain, strict-invalid
semantics, migration from force-endpoint
- docs/troubleshooting.md — full NodeSkipReason table, mesh-level
validation errors, status conditions,
common pitfalls
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs: add known-gaps handoff document
Add docs/known-gaps.md tracking outstanding work, divergences from the
upstream proposal (cozystack/community#7), and settled design decisions
that should not be re-litigated. Link from README documentation table
and project-status note.
Captures one blocker gap (no Node watches), one operational risk
(silent anchor-peer suppression), and three proposal text corrections
(annotation name, prefix rule, flat-vs-typed CRD shape).
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* test(integration): provide resolvable endpoint for nodes
The endpoint resolution chain introduced in f8e0ab2 now rejects nodes that
have neither a clustermesh-endpoint, a force-endpoint, nor an ExternalIP.
The existing fixtures created nodes with no endpoint source at all, so
ValidateNode skipped them with NodeNoEndpoint and the integration tests
that asserted on resulting peer counts started failing in CI.
Attach a default ExternalIP to every node built by makeNode so that the
ExternalIP fallback succeeds, and persist Status.Addresses via the status
subresource (Create does not). The TestHappyPath endpoint assertion is
tightened to target the peer for the node that carries the explicit
force-endpoint, since other peers now legitimately resolve via the
ExternalIP fallback.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* ci(workflows): disable checkout credential persistence
Add persist-credentials: false to every actions/checkout step so the
GITHUB_TOKEN is not retained in the workspace after the action exits.
This is defense-in-depth: jobs that need authenticated access (image
publish) authenticate explicitly via docker/login-action, and the
remaining jobs are read-only consumers of the checked-out tree.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(installation): set yaml language on RBAC fenced block
Address markdownlint MD040 on the ClusterRole snippet.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(installation): strip prompt prefixes from no-output examples
Remove the leading "$ " from single-command shell examples that show
no command output. Addresses markdownlint MD014.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(per-node): set text language on plain fenced blocks
Add text language identifier to the four plain code fences (priority
list, host:port template, IPv6 example, bracketed DNS example).
Addresses markdownlint MD040.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
* docs(readme): merge adjacent blockquote callouts
Replace the blank separator between the CIDR-overlap warning and the
CRD self-install note with a quoted blank line so both callouts live
inside one continuous blockquote. Addresses markdownlint MD028.
Signed-off-by: Arsolitt <arsolitt@gmail.com>
---------
Signed-off-by: Arsolitt <arsolitt@gmail.com>1 parent f0e9b03 commit d719243
52 files changed
Lines changed: 3987 additions & 411 deletions
File tree
- .github/workflows
- api/v1alpha1
- charts/kilo-clustermesh-operator
- templates
- tests
- cmd
- config/crd/bases
- docs
- internal
- citest
- containerfile
- controller
- crd
- kilonode
- netutil
- peer
- restart
- validation
- pkg/kilo/v1alpha1
- test/integration
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
5 | | - | |
| 5 | + | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| |||
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| 27 | + | |
| 28 | + | |
25 | 29 | | |
26 | 30 | | |
27 | 31 | | |
28 | | - | |
| 32 | + | |
29 | 33 | | |
30 | 34 | | |
31 | 35 | | |
| |||
35 | 39 | | |
36 | 40 | | |
37 | 41 | | |
| 42 | + | |
| 43 | + | |
38 | 44 | | |
39 | 45 | | |
40 | 46 | | |
| |||
47 | 53 | | |
48 | 54 | | |
49 | 55 | | |
| 56 | + | |
| 57 | + | |
50 | 58 | | |
51 | 59 | | |
52 | 60 | | |
| |||
56 | 64 | | |
57 | 65 | | |
58 | 66 | | |
| 67 | + | |
| 68 | + | |
59 | 69 | | |
60 | | - | |
| 70 | + | |
61 | 71 | | |
62 | 72 | | |
63 | 73 | | |
64 | 74 | | |
65 | 75 | | |
66 | 76 | | |
67 | 77 | | |
| 78 | + | |
| 79 | + | |
68 | 80 | | |
69 | 81 | | |
70 | 82 | | |
71 | 83 | | |
72 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
140 | 140 | | |
141 | 141 | | |
142 | 142 | | |
| 143 | + | |
143 | 144 | | |
144 | 145 | | |
145 | 146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
45 | 45 | | |
46 | 46 | | |
47 | 47 | | |
48 | | - | |
| 48 | + | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
0 commit comments