Skip to content

Commit 11c5b6f

Browse files
authored
feat: add controller initializer (#7)
1 parent e3ec427 commit 11c5b6f

File tree

7 files changed

+360
-55
lines changed

7 files changed

+360
-55
lines changed

controller/initializer/initializer.go

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/**
2+
* Copyright 2023 The KusionStack Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package initializer
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"sync"
23+
24+
"github.com/spf13/pflag"
25+
"k8s.io/apimachinery/pkg/util/sets"
26+
"sigs.k8s.io/controller-runtime/pkg/manager"
27+
)
28+
29+
// InitFunc is used to launch a particular controller. It may run additional "should I activate checks".
30+
// Any error returned will cause the controller process to `Fatal`
31+
// The bool indicates whether the controller was enabled.
32+
type InitFunc func(manager.Manager) (enabled bool, err error)
33+
34+
// InitOption configures how we set up initializer
35+
type InitOption interface {
36+
apply(*options)
37+
}
38+
39+
type options struct {
40+
disableByDefault bool
41+
}
42+
type optionFunc func(*options)
43+
44+
func (o optionFunc) apply(opt *options) {
45+
o(opt)
46+
}
47+
48+
// WithDisableByDefault disable controller by default
49+
func WithDisableByDefault() InitOption {
50+
return optionFunc(func(o *options) {
51+
o.disableByDefault = true
52+
})
53+
}
54+
55+
// Interface knows how to set up controllers with manager
56+
type Interface interface {
57+
// Add add new controller setup function to initializer.
58+
Add(controllerName string, setup InitFunc, options ...InitOption) error
59+
60+
// KnownControllers returns a slice of strings describing the ControllerInitialzier's known controllers.
61+
KnownControllers() []string
62+
63+
// Enabled returns true if the controller is enabled.
64+
Enabled(name string) bool
65+
66+
// SetupWithManager add all enabled controllers to manager
67+
SetupWithManager(mgr manager.Manager) error
68+
69+
// BindFlag adds a flag for setting global feature gates to the specified FlagSet.
70+
BindFlag(fs *pflag.FlagSet)
71+
}
72+
73+
// New returns a new instance of initializer interface
74+
func New() Interface {
75+
return &controllerInitializer{
76+
initializers: make(map[string]InitFunc),
77+
all: sets.NewString(),
78+
enabled: sets.NewString(),
79+
disableByDefault: sets.NewString(),
80+
}
81+
}
82+
83+
var _ Interface = &controllerInitializer{}
84+
85+
type controllerInitializer struct {
86+
lock sync.RWMutex
87+
88+
initializers map[string]InitFunc
89+
all sets.String
90+
enabled sets.String
91+
disableByDefault sets.String
92+
}
93+
94+
func (m *controllerInitializer) Add(controllerName string, setup InitFunc, opts ...InitOption) error {
95+
m.lock.Lock()
96+
defer m.lock.Unlock()
97+
98+
if m.all.Has(controllerName) {
99+
return fmt.Errorf("controller %q has already been registered", controllerName)
100+
}
101+
102+
opt := &options{}
103+
for _, o := range opts {
104+
o.apply(opt)
105+
}
106+
107+
m.all.Insert(controllerName)
108+
109+
if opt.disableByDefault {
110+
m.disableByDefault.Insert(controllerName)
111+
} else {
112+
m.enabled.Insert(controllerName)
113+
}
114+
115+
m.initializers[controllerName] = setup
116+
return nil
117+
}
118+
119+
func (m *controllerInitializer) BindFlag(fs *pflag.FlagSet) {
120+
all := m.all.List()
121+
disabled := m.disableByDefault.List()
122+
fs.Var(m, "controllers", fmt.Sprintf(""+
123+
"A list of controllers to enable. '*' enables all on-by-default controllers, 'foo' enables the controller "+
124+
"named 'foo', '-foo' disables the controller named 'foo'.\nAll controllers: %s\nDisabled-by-default controllers: %s",
125+
strings.Join(all, ", "), strings.Join(disabled, ", ")))
126+
}
127+
128+
// KnownControllers implements ControllerInitialzier.
129+
func (m *controllerInitializer) KnownControllers() []string {
130+
m.lock.RLock()
131+
defer m.lock.RUnlock()
132+
return m.all.List()
133+
}
134+
135+
func (m *controllerInitializer) SetupWithManager(mgr manager.Manager) error {
136+
m.lock.RLock()
137+
defer m.lock.RUnlock()
138+
139+
for _, name := range m.enabled.List() {
140+
_, err := m.initializers[name](mgr)
141+
if err != nil {
142+
return fmt.Errorf("failed to initialize controller %q: %v", name, err)
143+
}
144+
}
145+
return nil
146+
}
147+
148+
func (m *controllerInitializer) Enabled(name string) bool {
149+
m.lock.RLock()
150+
defer m.lock.RUnlock()
151+
152+
return m.enabled.Has(name)
153+
}
154+
155+
func (m *controllerInitializer) isControllerEnabled(name string, controllers []string) bool {
156+
hasStar := false
157+
for _, ctrl := range controllers {
158+
if ctrl == name {
159+
return true
160+
}
161+
if ctrl == "-"+name {
162+
return false
163+
}
164+
if ctrl == "*" {
165+
hasStar = true
166+
}
167+
}
168+
// if we get here, there was no explicit choice
169+
if !hasStar {
170+
// nothing on by default
171+
return false
172+
}
173+
174+
return !m.disableByDefault.Has(name)
175+
}
176+
177+
// Set implements pflag.Value interface.
178+
func (m *controllerInitializer) Set(value string) error {
179+
m.lock.Lock()
180+
defer m.lock.Unlock()
181+
182+
controllers := strings.Split(strings.TrimSpace(value), ",")
183+
all := m.all.List()
184+
for _, name := range all {
185+
if m.isControllerEnabled(name, controllers) {
186+
m.enabled.Insert(name)
187+
} else {
188+
m.enabled.Delete(name)
189+
}
190+
}
191+
return nil
192+
}
193+
194+
// Type implements pflag.Value interface.
195+
func (m *controllerInitializer) Type() string {
196+
return "stringSlice"
197+
}
198+
199+
// String implements pflag.Value interface.
200+
func (m *controllerInitializer) String() string {
201+
m.lock.RLock()
202+
defer m.lock.RUnlock()
203+
204+
pairs := []string{}
205+
for _, name := range m.all.List() {
206+
if m.enabled.Has(name) {
207+
pairs = append(pairs, name)
208+
} else {
209+
pairs = append(pairs, "-"+name)
210+
}
211+
}
212+
return strings.Join(pairs, ",")
213+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Copyright 2023 KusionStack Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package initializer
18+
19+
import (
20+
"testing"
21+
22+
"github.com/spf13/pflag"
23+
"github.com/stretchr/testify/assert"
24+
"sigs.k8s.io/controller-runtime/pkg/manager"
25+
)
26+
27+
func Test_Initialzier(t *testing.T) {
28+
initializer := New()
29+
err := initializer.Add("test1", testInitFunc)
30+
assert.NoError(t, err)
31+
err = initializer.Add("test2", testInitFunc)
32+
assert.NoError(t, err)
33+
err = initializer.Add("test3", testInitFunc, WithDisableByDefault())
34+
assert.NoError(t, err)
35+
36+
controllers := initializer.KnownControllers()
37+
assert.EqualValues(t, []string{"test1", "test2", "test3"}, controllers)
38+
assert.True(t, initializer.Enabled("test1"))
39+
assert.True(t, initializer.Enabled("test2"))
40+
assert.False(t, initializer.Enabled("test3"))
41+
42+
// duplicate
43+
err = initializer.Add("test1", testInitFunc)
44+
assert.Error(t, err)
45+
46+
// test bind flag
47+
fs := pflag.NewFlagSet("test-*", pflag.PanicOnError)
48+
initializer.BindFlag(fs)
49+
fs.Set("controllers", "*")
50+
err = fs.Parse(nil)
51+
assert.NoError(t, err)
52+
assert.True(t, initializer.Enabled("test1"))
53+
assert.True(t, initializer.Enabled("test2"))
54+
assert.False(t, initializer.Enabled("test3"))
55+
56+
fs = pflag.NewFlagSet("test", pflag.PanicOnError)
57+
initializer.BindFlag(fs)
58+
fs.Set("controllers", "test1,test2")
59+
err = fs.Parse(nil)
60+
assert.NoError(t, err)
61+
assert.True(t, initializer.Enabled("test1"))
62+
assert.True(t, initializer.Enabled("test2"))
63+
assert.False(t, initializer.Enabled("test3"))
64+
65+
fs = pflag.NewFlagSet("test", pflag.PanicOnError)
66+
initializer.BindFlag(fs)
67+
fs.Set("controllers", "-test1,test3")
68+
err = fs.Parse(nil)
69+
assert.NoError(t, err)
70+
assert.False(t, initializer.Enabled("test1"))
71+
assert.False(t, initializer.Enabled("test2"))
72+
assert.True(t, initializer.Enabled("test3"))
73+
74+
fs = pflag.NewFlagSet("test", pflag.PanicOnError)
75+
initializer.BindFlag(fs)
76+
fs.Set("controllers", "-test1")
77+
err = fs.Parse(nil)
78+
assert.NoError(t, err)
79+
assert.False(t, initializer.Enabled("test1"))
80+
assert.False(t, initializer.Enabled("test2"))
81+
assert.False(t, initializer.Enabled("test3"))
82+
}
83+
84+
func testInitFunc(manager.Manager) (enabled bool, err error) {
85+
return true, nil
86+
}

controller/mixin/mixin.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
// Copyright 2023 The KusionStack Authors
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
14-
1+
/**
2+
* Copyright 2023 KusionStack Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
1516
package mixin
1617

1718
import (

controller/mixin/mixin_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
// Copyright 2023 The KusionStack Authors
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
1+
/**
2+
* Copyright 2023 KusionStack Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
1416

1517
package mixin
1618

controller/mixin/webhook.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
// Copyright 2023 The KusionStack Authors
2-
//
3-
// Licensed under the Apache License, Version 2.0 (the "License");
4-
// you may not use this file except in compliance with the License.
5-
// You may obtain a copy of the License at
6-
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
8-
//
9-
// Unless required by applicable law or agreed to in writing, software
10-
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12-
// See the License for the specific language governing permissions and
13-
// limitations under the License.
1+
/**
2+
* Copyright 2023 KusionStack Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
1416

1517
package mixin
1618

0 commit comments

Comments
 (0)