Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server: make DI practical for controllers #4250

Merged
merged 4 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 27 additions & 12 deletions internal/cli/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 53 additions & 0 deletions internal/controllers/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package controllers

import (
"context"
"errors"
"fmt"

ctrl "sigs.k8s.io/controller-runtime"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/tilt-dev/tilt/internal/store"
)

type Controller interface {
reconcile.Reconciler
SetClient(client ctrlclient.Client)
SetupWithManager(mgr ctrl.Manager) error
}

type ControllerBuilder struct {
tscm *TiltServerControllerManager
controllers []Controller
}

func NewControllerBuilder(tscm *TiltServerControllerManager, controllers []Controller) *ControllerBuilder {
return &ControllerBuilder{
tscm: tscm,
controllers: controllers,
}
}

var _ store.Subscriber = &ControllerBuilder{}
var _ store.SetUpper = &ControllerBuilder{}

func (c *ControllerBuilder) OnChange(_ context.Context, _ store.RStore) {}

func (c *ControllerBuilder) SetUp(_ context.Context, _ store.RStore) error {
mgr := c.tscm.GetManager()
client := c.tscm.GetClient()

if mgr == nil || client == nil {
return errors.New("controller manager not initialized")
}

for _, controller := range c.controllers {
controller.SetClient(client)
if err := controller.SetupWithManager(mgr); err != nil {
return fmt.Errorf("error initializing %T controller: %v", controller, err)
}
}
return nil
}
54 changes: 54 additions & 0 deletions internal/controllers/core/filewatch/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
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 filewatch

import (
"context"

ctrl "sigs.k8s.io/controller-runtime"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/tilt-dev/tilt/internal/store"
filewatches "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
)

// FileWatchController reconciles a FileWatch object
type Controller struct {
ctrlclient.Client
Store store.RStore
}

func NewController(store store.RStore) *Controller {
return &Controller{
Store: store,
}
}

// +kubebuilder:rbac:groups=,resources=filewatches,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=,resources=filewatches/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=,resources=filewatches/finalizers,verbs=update

func (r *Controller) Reconcile(_ context.Context, _ ctrl.Request) (ctrl.Result, error) {
// this is currently a no-op stub
return ctrl.Result{}, nil
}

func (r *Controller) SetClient(client ctrlclient.Client) {
r.Client = client
}

func (r *Controller) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&filewatches.FileWatch{}).
Complete(r)
}
34 changes: 23 additions & 11 deletions internal/controllers/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,54 @@ import (
"fmt"

"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

"github.com/tilt-dev/tilt/internal/hud/server"
"github.com/tilt-dev/tilt/internal/store"
corev1alpha1 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
"github.com/tilt-dev/tilt/pkg/logger"
)

type TiltServerControllerManager struct {
config *rest.Config
cancel context.CancelFunc
config *rest.Config
scheme *runtime.Scheme
manager ctrl.Manager
cancel context.CancelFunc
}

var _ store.SetUpper = &TiltServerControllerManager{}
var _ store.Subscriber = &TiltServerControllerManager{}
var _ store.TearDowner = &TiltServerControllerManager{}

func NewTiltServerControllerManager(config *server.APIServerConfig) *TiltServerControllerManager {
func NewTiltServerControllerManager(config *server.APIServerConfig, scheme *runtime.Scheme) (*TiltServerControllerManager, error) {
return &TiltServerControllerManager{
config: config.GenericConfig.LoopbackClientConfig,
scheme: scheme,
}, nil
}

func (m *TiltServerControllerManager) GetManager() ctrl.Manager {
return m.manager
}

func (m *TiltServerControllerManager) GetClient() ctrlclient.Client {
if m.manager == nil {
return nil
}
return m.manager.GetClient()
}

func (m *TiltServerControllerManager) SetUp(ctx context.Context, st store.RStore) error {
scheme := runtime.NewScheme()

ctx, m.cancel = context.WithCancel(ctx)

// TODO(milas): we should provide a logr.Logger facade for our logger rather than using zap
w := logger.Get(ctx).Writer(logger.DebugLvl)
ctrl.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(w)))

utilruntime.Must(corev1alpha1.AddToScheme(scheme))

mgr, err := ctrl.NewManager(m.config, ctrl.Options{
Copy link
Member

Choose a reason for hiding this comment

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

can we dependency-inject the manager?

i think that would avoid a lot of the circular initialization issues you're running into

Copy link
Contributor Author

@milas milas Feb 25, 2021

Choose a reason for hiding this comment

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

Unfortunately the manager is the problem -- ctrl.NewManager creates a client that tries to establish a connection to API server and errors out if it's not up. (I originally tried to split the construction and .Start() call to do exactly what you're suggesting, which is how I discovered this mess)

Another possibility would be to create a DeferredClient that just returns errors until provided with a real implementation? That way there's no hard dependency and given that the client is only meant to be used within Reconcile() it shouldn't ever be able to get called before initialization anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Take a look at cc45e52 and see if you think that's an improvement? It really just lets controllers pretend to get created as expected by getting a ctrlclient.Client injected. The impl is *deferredClient that returns errors (or nil for methods that don't return error which is less than ideal) until the manager initializes it.

Not sure that the extra indirection is worth saving on needing to call SetClient(), but at least it hides some of the oddities from each controller implementation.

Scheme: scheme,
Scheme: m.scheme,
// controller manager lazily listens on a port if a webhook is registered; this functionality
// is currently NOT used; to prevent it from listening on a default port (9443) and potentially
// causing conflicts running multiple instances of tilt, this is set to an invalid value
Expand All @@ -58,15 +67,18 @@ func (m *TiltServerControllerManager) SetUp(ctx context.Context, st store.RStore
LeaderElectionID: "tilt-apiserver-ctrl",
})
if err != nil {
return fmt.Errorf("unable to start manager: %v", err)
return fmt.Errorf("unable to create controller manager: %v", err)
}

go func() {
if err := mgr.Start(ctx); err != nil && !errors.Is(err, context.Canceled) {
err = fmt.Errorf("controller manager stopped unexpectedly: %v", err)
st.Dispatch(store.NewErrorAction(err))
}
}()

m.manager = mgr

return nil
}

Expand Down
17 changes: 17 additions & 0 deletions internal/controllers/scheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package controllers

import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"

corev1alpha1 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
)

func NewScheme() *runtime.Scheme {
Copy link
Member

Choose a reason for hiding this comment

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

i think GenericConfig in apiserver.Config should have a scheme on it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, yep, it's on ExtraConfig. I'm hitting a funky issue though, not sure if we can share schemes between the actual API server and controller manager:

error initializing *filewatch.Controller controller: multiple group-version-kinds associated with type *v1alpha1.FileWatch, refusing to guess at one

Comes from https://github.com/kubernetes-sigs/controller-runtime/blob/e388e1e1f5a745166955442294bb8aede1f761e2/pkg/client/apiutil/apimachinery.go#L108-L113

The two versions are __internal and v1alpha1. The former comes from the API server's scheme registration:
https://github.com/tilt-dev/tilt-apiserver/blob/095950403c2cbc559e2f201da1c22e66b425e202/pkg/server/builder/resource/types.go#L119-L120

(Since it's kind of weird/indirect, when there are multi versions of an entity, the storage version is always going to get the __internal version in API server scheme.)

LMK if I'm missing something or doing something wrong - I am not familiar with the depths of the API server

Copy link
Member

Choose a reason for hiding this comment

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

i don't know, let's just check this in and i can debug what's going on...

scheme := runtime.NewScheme()

// modify pkg/apis/core/v1alpha1/register.go to get new types within core group registered
utilruntime.Must(corev1alpha1.AddToScheme(scheme))

return scheme
}
23 changes: 22 additions & 1 deletion internal/controllers/wire.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
package controllers

import "github.com/google/wire"
import (
"github.com/google/wire"

"github.com/tilt-dev/tilt/internal/controllers/core/filewatch"
)

var controllerSet = wire.NewSet(
filewatch.NewController,

ProvideControllers,
)

func ProvideControllers(fileWatch *filewatch.Controller) []Controller {
return []Controller{
fileWatch,
}
}

var WireSet = wire.NewSet(
NewTiltServerControllerManager,

NewScheme,
NewControllerBuilder,

controllerSet,
)
2 changes: 2 additions & 0 deletions internal/engine/subscribers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
func ProvideSubscribers(
hudsc *server.HeadsUpServerController,
tscm *controllers.TiltServerControllerManager,
cb *controllers.ControllerBuilder,
hud hud.HeadsUpDisplay,
ts *hud.TerminalStream,
tp *prompt.TerminalPrompt,
Expand Down Expand Up @@ -58,6 +59,7 @@ func ProvideSubscribers(
// The controller manager must go after the API server,
// so that it can connect to it and make resources available.
tscm,
cb,

hud,
ts,
Expand Down
6 changes: 4 additions & 2 deletions internal/engine/upper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3776,7 +3776,9 @@ func newTestFixture(t *testing.T) *testFixture {
tp := prompt.NewTerminalPrompt(ta, prompt.TTYOpen, prompt.BrowserOpen,
log, "localhost", model.WebURL{})
h := hud.NewFakeHud()
tscm := controllers.NewTiltServerControllerManager(serverOptions)
tscm, err := controllers.NewTiltServerControllerManager(serverOptions, controllers.NewScheme())
require.NoError(t, err, "Failed to create Tilt API server controller manager")
cb := controllers.NewControllerBuilder(tscm, nil)

dp := dockerprune.NewDockerPruner(dockerClient)
dp.DisabledForTesting(true)
Expand Down Expand Up @@ -3819,7 +3821,7 @@ func newTestFixture(t *testing.T) *testFixture {
mc := metrics.NewController(de, model.TiltBuild{}, "")
mcc := metrics.NewModeController("localhost", user.NewFakePrefs())

subs := ProvideSubscribers(hudsc, tscm, h, ts, tp, pw, sw, plm, pfc, fwm, gm, bc, cc, dcw, dclm, pm, ar, au, ewm, tcum, dp, tc, lc, podm, ec, mc, mcc)
subs := ProvideSubscribers(hudsc, tscm, cb, h, ts, tp, pw, sw, plm, pfc, fwm, gm, bc, cc, dcw, dclm, pm, ar, au, ewm, tcum, dp, tc, lc, podm, ec, mc, mcc)
ret.upper, err = NewUpper(ctx, st, subs)
require.NoError(t, err)

Expand Down
9 changes: 6 additions & 3 deletions internal/store/subscriber.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type Subscriber interface {
//
// Both hold the subscriber lock, so should return quickly.
//
// SetUp and TearDown are called in serial, in the order they were added.
// SetUp and TearDown are called in serial.
// SetUp is called in FIFO order while TearDown is LIFO so that
// inter-subscriber dependencies are respected.
type SetUpper interface {
// Initialize the subscriber.
//
Expand Down Expand Up @@ -91,14 +93,15 @@ func (l *subscriberList) SetUp(ctx context.Context, st RStore) error {
return nil
}

// TeardownAll removes subscribes in the reverse order as they were subscribed.
func (l *subscriberList) TeardownAll(ctx context.Context) {
l.mu.Lock()
subscribers := append([]*subscriberEntry{}, l.subscribers...)
l.setup = false
l.mu.Unlock()

for _, s := range subscribers {
s.maybeTeardown(ctx)
for i := len(subscribers) - 1; i >= 0; i-- {
subscribers[i].maybeTeardown(ctx)
}
}

Expand Down