Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d38b1bc
UPSTREAM: 120920: Revert: Revert: Clean up CRD conversion code structure
sttts Sep 11, 2023
6675174
README
sttts Sep 1, 2023
a3d62cb
UPSTREAM: <carry>: controlplane/apiserver: add miniaggregator
sttts Sep 27, 2023
fe47021
UPSTREAM: <carry>: clusterize controllers
sttts Sep 1, 2023
7100d0a
UPSTREAM: <carry>: storage: etcd cluster key computation
sttts Sep 27, 2023
e2232de
UPSTREAM: <carry>: clusterize serviceaccounts
sttts Sep 1, 2023
b66acc2
UPSTREAM: <carry>: registry/core/serviceaccount: do not crash without…
sttts Sep 27, 2023
eeeb754
UPSTREAM: <carry>: INTERESTING: clusterize admission
sttts Sep 1, 2023
50d1d06
UPSTREAM: <carry>: Clusterize ValidatingAdmissionPolicy admission plu…
embik Jul 24, 2024
b743da5
UPSTREAM: <carry>: clusterize storage hash
sttts Sep 27, 2023
1d7e7fa
UPSTREAM: <carry>: endpoints: add Cluster struct for ctx
sttts Sep 27, 2023
d22f103
UPSTREAM: <carry>: endpoints/patch: wire openapi for CRD strategic me…
sttts Sep 27, 2023
87ee290
UPSTREAM: <carry>: storage/etcd3: clusterize
sttts Sep 27, 2023
568d3e8
UPSTREAM: <carry>: apiserver: partial wildcard metadata request accro…
sttts Sep 27, 2023
32ef620
UPSTREAM: <carry>: endpoints: set kcp.io/original-api-version on wild…
sttts Sep 27, 2023
86484fb
UPSTREAM: <carry>: watch(er/cache): clusterize
sttts Sep 27, 2023
acd0f8b
UPSTREAM: <carry>: apiserver: split chain into pre and post authz
sttts Sep 27, 2023
17f27f4
UPSTREAM: <carry>: apiserver: clusterize listed paths
sttts Sep 27, 2023
8b3765f
UPSTREAM: <carry>: apiserver: clusterize OpenAPI v2
sttts Sep 27, 2023
8085762
UPSTREAM: <carry>: clusterize BuiltInAuthenticationOptions
sttts Sep 27, 2023
dfc70aa
UPSTREAM: <carry>: storage: add UseResourceAsPrefixDefault for legacy…
sttts Sep 27, 2023
15447c6
UPSTREAM: <carry>: apiextensions-apiserver
sttts Sep 27, 2023
2985957
UPSTREAM: <carry>: cache-server: wire shard name into storage
sttts Sep 27, 2023
50365e5
UPSTREAM: <carry>: controlplane: wire informers and clients
sttts Sep 11, 2023
82c719e
UPSTREAM: <carry>: generic cleanup
mjudeikis Sep 3, 2024
a658034
UPSTREAM: <carry>: pass system:admin clients and informers in generic…
embik Jun 13, 2024
04de68d
UPSTREAM: <carry>: remove REST mapper from admission plugins
embik Jun 20, 2024
0ffbc0a
UPSTREAM: <carry>: provide supportedMediaTypes for custom resoure han…
embik Jun 21, 2024
6e59d68
UPSTREAM: <carry>: prevent NPE if no authorization is set
embik Jun 21, 2024
726c120
UPSTREAM: <carry>: wrap CRD group into packagePrefix for OpenAPIV3 bu…
embik Jul 12, 2024
3c2e936
UPSTREAM: <CARRY>: clusterize validatingadmissionpolicystatus controller
embik Jul 23, 2024
a583d08
UPSTREAM: <carry>: apiserver cleaning
mjudeikis Sep 3, 2024
01c33de
UPSTREAM: <carry>: include cluster name in authz SubjectAccessReview …
xrstf Nov 13, 2024
9468abc
UPSTREAM: <carry>: split auth/authz chains even more
mjudeikis Dec 10, 2024
c8d1f35
UPSTREAM: <carry>: authz: add scoping to default rule resolver
sttts Aug 23, 2024
1d3c908
UPSTREAM: <carry>: authz: add warrants to default rule resolver
sttts Aug 23, 2024
1c3d791
UPSTREAM: <carry>: Clusterize MutatingAdmissionPolicy admission plugi…
gman0 Jan 23, 2025
68a7b46
UPSTREAM: <carry>: Add kube feature gate for global service account
cnvergence Mar 13, 2025
d115301
UPSTREAM: <carry>: Allow pin-dependency to add redirects to local paths
ntnn Jul 16, 2025
4cab8e2
UPSTREAM: <carry>: Drop validation-gen
ntnn Aug 4, 2025
467aa93
UPSTREAM: <carry>: Drop merge and replace in x-kubernetes-list/map-type
mjudeikis Aug 14, 2025
31165a2
UPSTREAM: <carry>: Keep system:cluster:* groups
ntnn Aug 12, 2025
4b7b5c8
UPSTREAM: <carry>: Rename authentication.{kubernetes=>kcp}.io/cluster…
ntnn Aug 28, 2025
1e1cb8a
UPSTREAM: <carry>: Tie global service account test result to feature …
ntnn Sep 29, 2025
fc3e360
UPSTREAM: <carry>: Add authentication.kcp.io/scopes to service accounts
ntnn Sep 10, 2025
ac890f5
UPSTREAM: <carry>: Add system:cluster:<cluster> group to effective users
ntnn Sep 10, 2025
5995c62
UPSTREAM: <carry>: Update pkg/registry/rbac/validation/kcp_test.go
ntnn Sep 15, 2025
f47e77e
UPSTREAM: <carry>: Add authorization.kcp.io/cluster-name extra to web…
ntnn Sep 29, 2025
d90ff83
UPSTREAM: <fixup>: Remove redundant failsafe
ntnn Sep 30, 2025
136b0ba
UPSTREAM: <carry>: forward current cluster context in TokenReviews
xrstf Oct 2, 2025
edf4fc1
UPSTREAM: <carry>: kube-aggregator
sttts Sep 1, 2023
5d5c9ec
CARRY: Disable protoc support
mjudeikis Dec 8, 2025
6deabab
UPSTREAM: <carry>: add client and informer hacks
sttts Sep 27, 2023
34a93d3
UPSTREAM: <carry>: Add kcp patchers
sttts Oct 8, 2023
c4290e3
UPSTREAM: <carry>: gc & quata patches applied
mjudeikis Dec 8, 2025
71e0f25
UPSTREAM: <carry>: Clusterize cacher delegator
mjudeikis Dec 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions KCP_RELATED_CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Why this forked repository ?

This repository carries the prototype branch which accumulates the hacks, prototypes, proto-KEP experiments, and workarounds required to make [KCP](https://github.com/kcp-dev/kcp/blob/main/README.md) a reality.
It is based on K8S 1.22 for now and commits are identified with basic labels like HACK/FEATURE/WORKAROUND.

# Summary of changes

The detailed explanation of the changes made on top of the Kubernetes code can be found in both the commit messages, and comments of the associated code.

However here is a summary of the changes, with the underlying requirements and motivations. Reading the linked investigation document first will help.

## A. Minimal API Server

__Investigation document:__ [minimal-api-server.md](https://github.com/kcp-dev/kcp/blob/main/docs/investigations/minimal-api-server.md)

1. New generic control plane based on kube api-server

It is mainly provided by code:

1. initially duplicated from kube api-server main code, and legacy scheme (`DUPLICATE` commits),

2. then stripped down from unnecessary things (ergress, api aggregation, webhooks) and APIs (Pods, Nodes, Deployments, etc ...) (`NEW` commits)

2. Support adding K8S built-in resources (`core/Pod`, `apps/Deployment`, ...) as CRDs

This is required since the new generic control plane scheme doesn't contain those resources any more.

This is provided by:

- hacks (`HACK` commits) that:

1. <a id="A-2-1"></a> allow the go-restful server to be bypassed for those resources, and route them to the CRD handler
2. <a id="A-2-2"></a> allow the CRD handler, and opanapi publisher, to support resources of the `core` group
3. <a id="A-2-3"></a> convert the `protobuf` requests sent to those resources resources to requests in the `application/json` content type, before letting the CRD handler serve them
4. <a id="A-2-4"></a> replace the table converter of CRDs that bring back those resources, with the default table converter of the related built-in resource

- a new feature, or potential kube fix (`KUBEFIX` commit), that:

5. <a id="A-2-5"></a> introduces the support of strategic merge patch for CRDs.
This support uses the OpenAPI v3 schema of the CRD to drive the SMP execution, but only adds a minimal implementation and doesn't fully support OpenAPI schemas that don't have expected `patchStrategy` and `patchMergeKey` annotations.
In order to avoid changing the behavior of existing client tools, the support is only added for those K8S built-in resources

## B. Logical clusters

__Investigation document:__ [logical-clusters.md](https://github.com/kcp-dev/kcp/blob/main/docs/investigations/logical-clusters.md)

1. Logical clusters represented as a prefix in etcd

It is mainly provided by hacks (`HACK` commits) that:

1. <a id="B-1-1"></a> allow intercepting the api server handler chain to set the expected logical cluster context value from either a given suffix in the request APIServer base URL, or a given header in the http request

2. <a id="B-1-2"></a> change etcd storage layer in order to use the logical cluster as a prefix in the etcd key

3. <a id="B-1-3"></a> allow wildcard watches that retrieve objects from all the logical clusters

4. <a id="B-1-4"></a> correctly get or set the `clusterName` metadata field in the storage layer operations based on the etcd key and its new prefix

2. Support of logical clusters (== tenancy) in the CRD management, OpenAPI and discovery endpoints, and clients used by controllers

<a id="B-2"></a>It is mainly provided by a hack (`HACK` commit) that adds CRD tenancy by ensuring that logical clusters are taken in account in:
- CRD-related controllers
- APIServices-related controllers
- Discovery + OpenAPI endpoints

In the current Kubernetes design, those 3 areas are highly coupled and intricated, which explains why this commit had to hack the code at various levels:
- client-go level
- controllers level,
- http handlers level.

While this gives a detailed idea of which code needs to be touched in order to enable CRD tenancy, a clean implementation would first require some refactoring, in order to build the required abstraction layers that would allow decoupling those areas.

# Potential client problems

Although these changes in the K8S codebase were made in order to keep the compatibility with Kuberntes client tools, there might be some problems:

## Incomplete protobuf support for built-in resources

In some contexts, like the `controller-runtime` library used by the Operator SDK, all the resources of the `client-go` scheme are created / updated using the `application/vnd.kubernetes.protobuf` content type.

However when these resources are in fact added as CRDs, in the KCP minimal API server scenario, these resources cannot be created / updated since the protobuf (de)serialization is not (and probably cannot be) supported for CRDs.
So for now in this case, the [A.2.3 hack mentioned above](#A-2-3) just converts the `protobuf` request to a `json` one, but this might not cover all the use-cases or corner cases.

The clean solution would probably be the negotiation of serialization type in `client-go`, which we haven't implemented yet, but which would work like this:
When a request for an unsupported serialization is returned, the server should reject it with a 406
and provide a list of supported content types. `client-go` should then examine whether it can satisfy such a request by encoding the object with a different scheme.
This would require a KEP but at least is in keeping with content negotiation on GET / WATCH in Kube

## Incomplete Strategic merge patch support for built-in resources

Client tools like `kubectl` assume that all K8S native resources (== `client-go` schema resources)
support strategic merge patch, and use it by default when updating or patching a resource.

In Kube, currently, strategic merge patch is not supported for CRDs, which would break compatibility with client tools for all the K8S natives resources that are in fact added as CRD in the KCP minimal api server.
The [A-2-5 change mentioned above](#A-2-5) tries to fix this by using the CRD openAPI v3 schema as the source of the required information that will drive the strategic merge patch execution.

While this fixes the problem in most cases, there might still be errors in case the OpenAPI v2 schema for such a resource is missing `x-kubernetes-patch-strategy` and `x-kubernetes-patch-merge-key` annotations when imported from the CRD OpenAPI v3 schema.
14 changes: 10 additions & 4 deletions cmd/kube-apiserver/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ package app

import (
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/informerfactoryhack"
"k8s.io/apiserver/pkg/util/webhook"
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver"
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme"

"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/controlplane"
Expand Down Expand Up @@ -92,14 +93,19 @@ func NewConfig(opts options.CompletedOptions) (*Config, error) {
}
c.KubeAPIs = kubeAPIs

apiExtensions, err := controlplaneapiserver.CreateAPIExtensionsConfig(*kubeAPIs.ControlPlane.Generic, kubeAPIs.ControlPlane.VersionedInformers, pluginInitializer, opts.CompletedOptions, opts.MasterCount,
serviceResolver, webhook.NewDefaultAuthenticationInfoResolverWrapper(kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Generic.EgressSelector, kubeAPIs.ControlPlane.Generic.LoopbackClientConfig, kubeAPIs.ControlPlane.Generic.TracerProvider))
authInfoResolver := webhook.NewDefaultAuthenticationInfoResolverWrapper(kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Generic.EgressSelector, kubeAPIs.ControlPlane.Generic.LoopbackClientConfig, kubeAPIs.ControlPlane.Generic.TracerProvider)
conversionFactory, err := conversion.NewCRConverterFactory(serviceResolver, authInfoResolver)
if err != nil {
return nil, err
}

apiExtensions, err := controlplaneapiserver.CreateAPIExtensionsConfig(*kubeAPIs.ControlPlane.Generic, informerfactoryhack.Wrap(versionedInformers), pluginInitializer, opts.CompletedOptions, opts.MasterCount, conversionFactory)
if err != nil {
return nil, err
}
c.ApiExtensions = apiExtensions

aggregator, err := controlplaneapiserver.CreateAggregatorConfig(*kubeAPIs.ControlPlane.Generic, opts.CompletedOptions, kubeAPIs.ControlPlane.VersionedInformers, serviceResolver, kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Extra.PeerProxy, pluginInitializer)
aggregator, err := controlplaneapiserver.CreateAggregatorConfig(*kubeAPIs.ControlPlane.Generic, opts.CompletedOptions, informerfactoryhack.Wrap(versionedInformers), serviceResolver, kubeAPIs.ControlPlane.ProxyTransport, kubeAPIs.ControlPlane.Extra.PeerProxy, pluginInitializer)
if err != nil {
return nil, err
}
Expand Down
8 changes: 5 additions & 3 deletions cmd/kube-apiserver/app/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ import (
"net/url"
"os"

kcpinformers "github.com/kcp-dev/client-go/informers"
"github.com/spf13/cobra"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
"k8s.io/apiserver/pkg/informerfactoryhack"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/server/egressselector"
serverstorage "k8s.io/apiserver/pkg/server/storage"
Expand Down Expand Up @@ -187,7 +189,7 @@ func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregat
}

// aggregator comes last in the chain
aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer(config.Aggregator, kubeAPIServer.ControlPlane.GenericAPIServer, apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdAPIEnabled, apiVersionPriorities)
aggregatorServer, err := controlplaneapiserver.CreateAggregatorServer(config.Aggregator, kubeAPIServer.ControlPlane.GenericAPIServer, apiExtensionsServer.Informers.Apiextensions().V1().CustomResourceDefinitions().Cluster(controlplaneapiserver.LocalAdminCluster), crdAPIEnabled, apiVersionPriorities)
if err != nil {
// we don't need special handling for innerStopCh because the aggregator server doesn't create any go routines
return nil, err
Expand All @@ -200,7 +202,7 @@ func CreateServerChain(config CompletedConfig) (*aggregatorapiserver.APIAggregat
func CreateKubeAPIServerConfig(
opts options.CompletedOptions,
genericConfig *genericapiserver.Config,
versionedInformers clientgoinformers.SharedInformerFactory,
versionedInformers kcpinformers.SharedInformerFactory,
storageFactory *serverstorage.DefaultStorageFactory,
) (
*controlplane.Config,
Expand All @@ -218,7 +220,7 @@ func CreateKubeAPIServerConfig(
return nil, nil, nil, fmt.Errorf("failed to create admission plugin initializer: %w", err)
}

serviceResolver, err := buildServiceResolver(opts.EnableAggregatorRouting, genericConfig.LoopbackClientConfig.Host, versionedInformers)
serviceResolver, err := buildServiceResolver(opts.EnableAggregatorRouting, genericConfig.LoopbackClientConfig.Host, informerfactoryhack.Wrap(versionedInformers))
if err != nil {
return nil, nil, nil, fmt.Errorf("error building service resolver: %w", err)
}
Expand Down
6 changes: 5 additions & 1 deletion cmd/kube-controller-manager/app/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
genericfeatures "k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/quota/v1/generic"
utilfeature "k8s.io/apiserver/pkg/util/feature"
Expand Down Expand Up @@ -577,7 +579,9 @@ func startModifiedNamespaceController(ctx context.Context, controllerContext Con
return nil, true, err
}

discoverResourcesFn := namespaceKubeClient.Discovery().ServerPreferredNamespacedResources
discoverResourcesFn := func(clusterName logicalcluster.Name) ([]*metav1.APIResourceList, error) {
return namespaceKubeClient.Discovery().ServerPreferredNamespacedResources()
}

namespaceController := namespacecontroller.NewNamespaceController(
ctx,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/karrick/godirwalk v1.17.0 // indirect
github.com/kcp-dev/logicalcluster/v3 v3.0.5 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI=
github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=
github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU=
github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
Expand Down
109 changes: 109 additions & 0 deletions hack/kcp/garbage_collector_patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2022 The KCP 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 main

import (
"bufio"
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"strings"
)

/*
Process:

1. go run ./hack/kcp/garbage_collector_patch.go > pkg/controller/garbagecollector/garbagecollector_kcp.go
(you may need to add -mod=readonly)

2. goimports -w pkg/controller/garbagecollector/garbagecollector_kcp.go

3. reapply patch for kcp to pkg/controller/garbagecollector/garbagecollector_kcp.go
*/

func main() {
fileSet := token.NewFileSet()

file, err := parser.ParseFile(fileSet, "pkg/controller/garbagecollector/garbagecollector.go", nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}

// n stores a reference to the node for the function declaration for Sync
var n ast.Node

ast.Inspect(file, func(node ast.Node) bool {
switch x := node.(type) {
case *ast.FuncDecl:
if x.Name.Name == "Sync" {
// Store the reference
n = node
// Stop further inspection
return false
}
}

// Continue recursing
return true
})

startLine := fileSet.Position(n.Pos()).Line
endLine := fileSet.Position(n.End()).Line

// To preserve the comments from within the function body itself, we have to write out the entire file to a buffer,
// then extract only the lines we care about (the function body).
var buf bytes.Buffer
if err := format.Node(&buf, fileSet, file); err != nil {
log.Fatal(err)
}

// Convert the buffer to a slice of lines, so we can grab the portion we want
var lines []string
scanner := bufio.NewScanner(&buf)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}

fmt.Println(`/*
Copyright 2022 The KCP 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 garbagecollector
`)

// Finally, print the line range we need
fmt.Println(strings.Join(lines[startLine-1:endLine], "\n"))
}
Loading