Skip to content

WIP: ✨ Cluster Provider and cluster-aware controllers - #2207

Closed
vincepri wants to merge 1 commit into
kubernetes-sigs:mainfrom
vincepri:logical-cluster-support
Closed

vincepri wants to merge 1 commit into
kubernetes-sigs:mainfrom
vincepri:logical-cluster-support

Conversation

@vincepri

Copy link
Copy Markdown
Member

No description provided.

@k8s-ci-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: vincepri

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Feb 21, 2023
@k8s-ci-robot k8s-ci-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Feb 21, 2023
Comment thread pkg/builder/controller.go Resolved
Comment thread pkg/builder/controller.go Resolved
Comment thread pkg/builder/controller.go Resolved
Comment thread pkg/builder/controller_test.go Resolved
}
cluster1, err := mgr.LogicalClusterGetter()("cluster1")
Expect(err).NotTo(HaveOccurred())
Expect(cluster1.GetClient().Create(ctx, dep)).To(Succeed())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Do I see correctly that you are creating a Deployment on Cluster1 and then get reconcile requests for Cluster1 and Cluster2?

I would have expected to only get one on Cluster1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's mostly because under the hood it's returning the same testenv kubeconfig

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for now, need to rework testenv a bit potentially to spin up multiple clusters

@sbueringer sbueringer Feb 28, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I guessed it is something like that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case we don't want to adress this on this PR, let's add a TODO to make clear the currently tested/verified behavior is not the actual behavior

Comment thread pkg/internal/controller/controller.go Resolved
Comment thread pkg/manager/internal.go Resolved
Comment thread pkg/manager/internal.go Resolved
Comment thread pkg/manager/internal.go Resolved
Comment thread pkg/manager/manager_test.go Resolved
Comment thread pkg/controller/controller.go Resolved
Comment thread pkg/handler/enqueue.go Resolved
Comment thread pkg/internal/controller/controller.go Outdated

// Cluster is the logical cluster that this controller is running against.
// +optional
Cluster logical.Name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand why a controller would need to know anything about the cluster it runs against. It has eventsources and the events those emit contain the cluster. It should also be mentioned that a controller might be getting events from multiple clusters.

IMHO we should just add the cluster field based on the reconcile.Request into the logger (which is from what I can tell the only place where we use this)

Comment thread pkg/manager/manager.go Resolved
Comment thread pkg/manager/manager.go Resolved
Comment thread pkg/reconcile/reconcile.go Resolved
@vincepri
vincepri force-pushed the logical-cluster-support branch 2 times, most recently from e561bb9 to 64ecb97 Compare February 27, 2023 20:06
@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed size/XL Denotes a PR that changes 500-999 lines, ignoring generated files. labels Feb 28, 2023
@vincepri
vincepri force-pushed the logical-cluster-support branch 2 times, most recently from e29fcad to 153e775 Compare February 28, 2023 19:11
@vincepri

Copy link
Copy Markdown
Member Author

/test pull-controller-runtime-test

@vincepri
vincepri force-pushed the logical-cluster-support branch 6 times, most recently from ce17906 to 8d3b8f7 Compare March 1, 2023 22:44
Comment thread pkg/manager/internal.go
var watchErr error
if err := wait.PollImmediateUntilWithContext(ctx, 1*time.Second, func(ctx context.Context) (done bool, err error) {
cfg, watchErr = cm.logicalAdapter.RESTConfig(name)
cl, watchErr = cm.logicalProvider.Get(ctx, name, cm.defaultClusterOptions, cluster.WithName(name))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why both name and cluster.WithName(name)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last one just sets the option for cluster.New

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to rework it, I guess it's a bit weird 😄

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why poll rather than making Get blocking?

@vincepri
vincepri force-pushed the logical-cluster-support branch from 9618908 to 9d51b73 Compare March 3, 2023 18:15
@vincepri vincepri changed the title dnr: wip: logical cluster WIP: ✨ Cluster Provider and cluster-aware controllers Mar 3, 2023
@k8s-ci-robot k8s-ci-robot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Mar 3, 2023
@vincepri
vincepri force-pushed the logical-cluster-support branch 3 times, most recently from c648112 to 13a0b91 Compare March 6, 2023 14:57
@vincepri
vincepri force-pushed the logical-cluster-support branch from 13a0b91 to 107c2dc Compare March 6, 2023 15:00

@sbueringer sbueringer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly just nits, but not entirely done with the review yet (pkg/manager is left)

Comment thread examples/fleet/main.go
// KindClusterProvider is a cluster provider that works with a local Kind instance.
type KindClusterProvider struct{}

func (k *KindClusterProvider) Get(ctx context.Context, name logical.Name, opts ...cluster.Option) (cluster.Cluster, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Might it be better if we end up with something like logicalcluster.Name or logical.ClusterName

Also in field names of structs like Builder:

	cluster          cluster.Cluster
	logicalName      logical.Name

vs.

	cluster          cluster.Cluster
	logicalClusterName      logicalcluster.Name

or if we want something shorter

	cluster          cluster.Cluster
	clusterName      logicalcluster.Name

(Might be good to consistently stick with the same naming, e.g. Cluster is a Cluster, ClusterName is a logicalcluster.Name, mixing it up might lead to a bit of confusion, e.g. with req.Cluster)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we should rename the repo to sigs.k8s.io/logicalcluster and change the package name to logicalcluster too. I also like clusterName as a short-hand for variables of type logicalcluster.Name.

@nikhita how would we do a repo rename?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative, as suggested in other comments: get rid of the alias altogether and just use string. Am undecided. Have done that now locally in this branch. We lose type-safety, and replace it with naming convention of calling every formally logical cluster name clusterName.

Comment thread examples/fleet/main.go Resolved
Comment thread pkg/builder/controller.go Resolved
Comment thread pkg/builder/controller.go
Comment on lines +338 to +341
// If we're building a logical controller, raw watches are not allowed
// given that the cache cannot be validated to be coming from the same cluter.
// In the future, we could consider allowing this by satisfying a new interface
// that sets and uses the cluster.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand a bit on that?

Is it that the cache that we get from cluster.GetCache might be caching objects from another cluster than the one the channel belongs to? (this is the case for source.Channel, right?)

How does this fit with this godoc comment?

 * Use Channel for events originating outside the cluster (eh.g. GitHub Webhook callback, Polling external urls).

I assume it's the case where folks are using Channel for something coming from a cluster? (which is not what we document, but of course possible)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, this makes it impossible to use sources that are not source.Kind

}
cluster1, err := mgr.LogicalClusterGetter()("cluster1")
Expect(err).NotTo(HaveOccurred())
Expect(cluster1.GetClient().Create(ctx, dep)).To(Succeed())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case we don't want to adress this on this PR, let's add a TODO to make clear the currently tested/verified behavior is not the actual behavior


func (e *enqueueRequestsFromMapFunc) DeepCopyFor(c cluster.Cluster) EventHandler {
return &enqueueRequestsFromMapFunc{
cluster: c,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if we need to store the entire Cluster given that we only use the name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related: at least in EnqueueRequestForObject we could additionally store the ClusterName in an additional field to avoid the lookup for every event

(Not sure if the compiler optimizes it away, but still simplifies the implementation)

mapper: c.GetRESTMapper(),
}
if err := copy.parseOwnerTypeGroupKind(c.GetScheme()); err != nil {
panic(err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Are there cases where it would be good if DeepCopyFor could return an error?


func (w *watchDescription) DeepCopyFor(c cluster.Cluster) *watchDescription {
copy := &watchDescription{
predicates: w.predicates,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This shares the predicates, right? Is that fine or is there any way we can copy them?

// This covers the case where a Watch was added later to the controller.
if watchDesc.IsClusterAware() {
for _, cldesc := range c.clusters {
if err := c.startClusterAwareWatchLocked(cldesc, watchDesc); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Are cluster aware watches started here and in l.192? If yes and it's intentional do we have to ensure that c.clusters does not contain the "default" cluster? Or is that the responsibility of the provider?

if watch == nil {
return nil
}
c.LogConstructor(nil).Info("Starting Cluster-Aware EventSource", "cluster", cldesc.Name(), "source", watch.src)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: not sure how watch.src is logged

Comment thread pkg/manager/internal.go Resolved
Comment thread pkg/manager/internal.go

// Disengage all the runnables.
for _, aware := range cm.logicalClusterAwareRunnables {
if err := aware.Disengage(c.ctx, c); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might make sense to collect error here to try removing it from as many runnables as possible and then return after the loop if we have some errors.

Comment thread pkg/manager/internal.go
if _, err := cm.getLogicalCluster(ctx, event.Name); err != nil {
return err
}
cm.engageClusterAwareRunnables()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we want to optimize this a bit in a follow-up to not always engage for all clusters, but just for the added/modified one.

Might become a problem with hundreds+ of clusters

// The runnables added before Start are started when Start is called.
// The runnables added after Start are started directly.
func (r *runnables) Add(fn Runnable) error {
// For wrapped logical runnables, we need to unwrap them to get the underlying runnable.

@sbueringer sbueringer Mar 8, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Do we still plan do implement something here or just leftover?

Comment thread pkg/manager/internal.go
// that we schedule all the cluster aware runnables.
for name, cluster := range cm.logicalClusters {
for _, aware := range cm.logicalClusterAwareRunnables {
if err := aware.Engage(cluster.ctx, cluster); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if this has to respect leader election as well

@sbueringer

Copy link
Copy Markdown
Member

Great step forward!!

Sorry also a bunch of nits, I thought I might as well just comment on them while I see them

Comment thread pkg/builder/controller.go
Comment on lines +338 to +341
// If we're building a logical controller, raw watches are not allowed
// given that the cache cannot be validated to be coming from the same cluter.
// In the future, we could consider allowing this by satisfying a new interface
// that sets and uses the cluster.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, this makes it impossible to use sources that are not source.Kind

Comment thread pkg/builder/controller.go Resolved
Comment thread pkg/builder/controller.go
Comment on lines +63 to +64
cluster cluster.Cluster
logicalName logical.Name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand these fields. The cluster is always set to the manager and logicalName is never set. Do you want to add a new WithCluster to the builder?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vincepri where did you want to go with this?

Comment thread pkg/cluster/cluster.go
// Cluster provides various methods to interact with a cluster.
type Cluster interface {
// Name returns the unique logical name of the cluster.
Name() logical.Name

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a string? I find the whole logical very confusing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 the current naming with logical is pretty confusing

Comment on lines +209 to +210
func (c *Controller) Engage(ctx context.Context, cluster cluster.Cluster) error {
c.mu.Lock()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is basically a ControllerBuilder. Why do we add logic into the controller how to duplicate itself rather than keeping this in the Builder layer above? i.E.

type ControllerBuilder func(cluster.Cluster) (cancel func() error, err error)

builder := builder.New(mgr).For(&myKind{}).ControllerBuilder(&MyReconciler{})

cancels := map[string]func() error{}
for clusterName, cluster := range mgr.GetClusters() {
  cancel, err := builder(cluster)
  if err != nil {
    return err
  }
  cancels[clusterName] = cancel
}

Adding this inside here is a) muddying the layers b) error prone if ppl change the controller in the future and miss the fact that everything has to be deepcopied in here

continue
}
// If the request doesn't specify a cluster, use the cluster from the context.
if req.Cluster == "" && e.cluster != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this kind of awareness into a custom mapper doesn't really make a lot of sense to me. It would only be useful in a controller that deals with multiple clusters, otherwise we don't need the distinction. Once we deal with multiple clusters, why would we only sometimes fill the Cluster field?

return nil
}

func (e *enqueueRequestForOwner) DeepCopyFor(c cluster.Cluster) EventHandler {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as in controller/source re keeping the builder layer above the thing

Comment thread pkg/manager/internal.go
}

func (cm *controllerManager) add(r Runnable) error {
if aware, ok := r.(cluster.AwareRunnable); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd much prefer to add an explicit RunForEachCluster(builder func(cluster.Cluster)(Runnable, error) that we can document, rather than requiring ppl to be aware of an extension interface.

It also means that we can simply use the existing Runnable interface to start/stop as needed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a sideeffect, this would address @sbueringer comment below re leader election

Comment thread pkg/manager/internal.go
var watchErr error
if err := wait.PollImmediateUntilWithContext(ctx, 1*time.Second, func(ctx context.Context) (done bool, err error) {
cfg, watchErr = cm.logicalAdapter.RESTConfig(name)
cl, watchErr = cm.logicalProvider.Get(ctx, name, cm.defaultClusterOptions, cluster.WithName(name))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why poll rather than making Get blocking?

Comment thread pkg/manager/internal.go
return cm.defaultCluster.GetAPIReader()
}

func (cm *controllerManager) engageClusterAwareRunnables() {

@alvaroaleman alvaroaleman Mar 19, 2023 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this feels extremely complicated. How about:

cancels := map[string]func(){}
clusterProvider.OnAdd(func(c cluster) error {
  for _, builder := range mgr.eachClusterRunnables {
     ctx, cancel := context.WithCancel(ctx)
     
      if err := mgr.Add(wrapRunnableWithCustomCtx(ctx, c)); err != nil { return err }
      c.GetCache().HasSynced()
      // maybe aggregate errs instead?
      runnable, err := builder(c);
      if err != nil { return err }
      if err := mgr.Add(wrapRunnableWithCustomCtx(ctx, runnable); err != nil { return err }
      cancels[cluster.Name() = cancel
  }
})

func wrapRunnableWithCustomCtx(ctx context.Context,r Runnable) Runnable {
  return RunnableFunc(_ context.Context){ r(ctx) }
}

clusterProvider.OnDelete(func(name string){
   for _, cancel := range cancels[name]{
    cancel()
   }
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clusterProvider is what you call logicalProvider. I find the logical in there still confusing, to me that sounds like there must be a physicalProvider somewhere - But that distinction doesn't make sense for controller runtime.

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 12, 2023
@vincepri
vincepri force-pushed the logical-cluster-support branch from 107c2dc to edecb30 Compare May 11, 2023 19:16
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label May 11, 2023
@vincepri
vincepri force-pushed the logical-cluster-support branch 2 times, most recently from 123d53c to 9e965d1 Compare May 11, 2023 19:19
@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 3, 2023
Signed-off-by: Vince Prignano <vincepri@redhat.com>
@vincepri
vincepri force-pushed the logical-cluster-support branch from 9e965d1 to dfeaa19 Compare July 5, 2023 19:49
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 5, 2023
@k8s-ci-robot

Copy link
Copy Markdown
Contributor

@vincepri: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
pull-controller-runtime-test dfeaa19 link true /test pull-controller-runtime-test
pull-controller-runtime-apidiff dfeaa19 link false /test pull-controller-runtime-apidiff

Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 27, 2023
@k8s-ci-robot

Copy link
Copy Markdown
Contributor

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

@k8s-triage-robot

Copy link
Copy Markdown

The Kubernetes project currently lacks enough contributors to adequately respond to all PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as fresh with /remove-lifecycle stale
  • Close this PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle stale

@k8s-ci-robot k8s-ci-robot added the lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. label Feb 17, 2024
@k8s-triage-robot

Copy link
Copy Markdown

The Kubernetes project currently lacks enough active contributors to adequately respond to all PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Mark this PR as fresh with /remove-lifecycle rotten
  • Close this PR with /close
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/lifecycle rotten

@k8s-ci-robot k8s-ci-robot added lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. and removed lifecycle/stale Denotes an issue or PR has remained open with no activity and has become stale. labels Mar 18, 2024
@k8s-triage-robot

Copy link
Copy Markdown

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Reopen this PR with /reopen
  • Mark this PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

@k8s-ci-robot

Copy link
Copy Markdown
Contributor

@k8s-triage-robot: Closed this PR.

Details

In response to this:

The Kubernetes project currently lacks enough active contributors to adequately respond to all issues and PRs.

This bot triages PRs according to the following rules:

  • After 90d of inactivity, lifecycle/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was applied, the PR is closed

You can:

  • Reopen this PR with /reopen
  • Mark this PR as fresh with /remove-lifecycle rotten
  • Offer to help out with Issue Triage

Please send feedback to sig-contributor-experience at kubernetes/community.

/close

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. lifecycle/rotten Denotes an issue or PR that has aged beyond stale and will be auto-closed. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants