Skip to content

Commit 76c5367

Browse files
authored
Merge branch 'main' into feat/readinesscheck
2 parents 8b23492 + 326ef9b commit 76c5367

32 files changed

+2150
-17
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ jobs:
1414

1515
steps:
1616
- name: Checkout code
17-
uses: actions/checkout@v4
17+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1818
with:
1919
submodules: recursive
2020

2121
- name: Set up Go
22-
uses: actions/setup-go@v5
22+
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5
2323
with:
2424
go-version-file: go.mod
2525

.github/workflows/release.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-24.04
1515
steps:
1616
- name: Checkout code
17-
uses: actions/checkout@v4
17+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1818
with:
1919
ssh-key: ${{ secrets.PUSH_KEY }}
2020
fetch-tags: true
@@ -72,7 +72,7 @@ jobs:
7272
7373
- name: Build Changelog
7474
id: github_release
75-
uses: mikepenz/release-changelog-builder-action@v5
75+
uses: mikepenz/release-changelog-builder-action@e92187bd633e680ebfdd15961a7c30b2d097e7ad # v5
7676
with:
7777
mode: "PR"
7878
configurationJson: |
@@ -106,7 +106,7 @@ jobs:
106106

107107
- name: Create GitHub release
108108
if: ${{ env.SKIP != 'true' }}
109-
uses: softprops/action-gh-release@v2
109+
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
110110
with:
111111
tag_name: ${{ env.version }}
112112
name: Release ${{ env.version }}

.github/workflows/reuse.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ jobs:
66
test:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v4
9+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
1010
- name: REUSE Compliance Check
11-
uses: fsfe/reuse-action@v5
11+
uses: fsfe/reuse-action@bb774aa972c2a89ff34781233d275075cbddf542 # v5

README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,47 @@ The library provides a wrapper around `logr.Logger`, exposing additional helper
270270
- There are several `FromContext...` functions for retrieving a logger from a `context.Context` object.
271271
- `InitFlags(...)` can be used to add the configuration flags for this logger to a cobra `FlagSet`.
272272

273+
### threads
274+
275+
The `threads` package provides a simple thread managing library. It can be used to run go routines in a non-blocking manner and provides the possibility to react if the routine has exited.
276+
277+
The most relevant use-case for this library in the context of k8s controllers is to handle dynamic watches on multiple clusters. To start a watch, that cluster's cache's `Start` method has to be used. Because this method is blocking, it has to be executed in a different go routine, and because it can return an error, a simple `go cache.Start(...)` is not enough, because it would hide the error.
278+
279+
#### Noteworthy Functions
280+
281+
- `NewThreadManager` creates a new thread manager.
282+
- The first argument is a `context.Context` used by the manager itself. Cancelling this context will stop the manager, and if the context contains a `logging.Logger`, the manager will use it for logging.
283+
- The second argument is an optional function that is executed after any go routine executed with this manager has finished. It is also possible to provide such a function for a specific go routine, instead for all of them, see below.
284+
- Use the `Run` method to start a new go routine.
285+
- Starting a go routine cancels the context of any running go routine with the same id.
286+
- This method also takes an optional function to be executed after the actual workload is done.
287+
- A on-finish function specified here is executed before the on-finish function of the manager is executed.
288+
- Note that go routines will wait for the thread manager to be started, if that has not yet happened. If the manager has been started, they will be executed immediately.
289+
- The thread manager will cancel the context that is passed into the workload function when the manager is being stopped. If any long-running commands are being run as part of the workload, it is strongly recommended to listen to the context's `Done` channel.
290+
- Use `Start()` to start the thread manager.
291+
- If any go routines have been added before this is called, they will be started now. New go routines added afterwards will be started immediately.
292+
- Calling this multiple times doesn't have any effect, unless the manager has already been stopped, in which case `Start()` will panic.
293+
- There are three ways to stop the thread manager again:
294+
- Use its `Stop()` method.
295+
- This is a blocking method that waits for all remaining go routines to finish. Their context is cancelled to notify them of the manager being stopped.
296+
- Cancel the context that was passed into `NewThreadManager` as the first argument.
297+
- Send a `SIGTERM` or `SIGINT` signal to the process.
298+
- The `TaskManager`'s `Restart`, `RestartOnError`, and `RestartOnSuccess` methods are pre-defined on-finish functions. They are not meant to be used directly, but instead be used as an argument to `Run`. See the example below.
299+
300+
#### Examples
301+
302+
```golang
303+
mgr := threads.NewThreadManager(ctx, nil)
304+
mgr.Start()
305+
// do other stuff
306+
// start a go routine that is restarted automatically if it finishes with an error
307+
mgr.Run(myCtx, "myTask", func(ctx context.Context) error {
308+
// my task coding
309+
}, mgr.RestartOnError)
310+
// do more other stuff
311+
mgr.Stop()
312+
```
313+
273314
### testing
274315

275316
This package contains useful functionality to aid with writing tests.
@@ -325,6 +366,81 @@ if readiness.IsReady() {
325366
}
326367
```
327368

369+
### Kubernetes resource management
370+
371+
The `pkg/resource` package contains some useful functions for working with Kubernetes resources. The `Mutator` interface can be used to modify resources in a generic way. It is used by the `Mutate` function, which takes a resource and a mutator and applies the mutator to the resource.
372+
The package also contains convenience types for the most common resource types, e.g. `ConfigMap`, `Secret`, `ClusterRole`, `ClusterRoleBinding`, etc. These types implement the `Mutator` interface and can be used to modify the corresponding resources.
373+
374+
#### Examples
375+
376+
Create or update a `ConfigMap`, a `ServiceAccount` and a `Deployment` using the `Mutator` interface:
377+
378+
```go
379+
type myDeploymentMutator struct {
380+
}
381+
382+
var _ resource.Mutator[*appsv1.Deployment] = &myDeploymentMutator{}
383+
384+
func newDeploymentMutator() resources.Mutator[*appsv1.Deployment] {
385+
return &MyDeploymentMutator{}
386+
}
387+
388+
func (m *MyDeploymentMutator) String() string {
389+
return "deployment default/test"
390+
}
391+
392+
func (m *MyDeploymentMutator) Empty() *appsv1.Deployment {
393+
return &appsv1.Deployment{
394+
ObjectMeta: metav1.ObjectMeta{
395+
Name: "test",
396+
Namespace: "default",
397+
},
398+
}
399+
}
400+
401+
func (m *MyDeploymentMutator) Mutate(deployment *appsv1.Deployment) error {
402+
// create one container with an image
403+
deployment.Spec.Template.Spec.Containers = []corev1.Container{
404+
{
405+
Name: "test",
406+
Image: "test-image:latest",
407+
},
408+
}
409+
return nil
410+
}
411+
412+
413+
func ReconcileResources(ctx context.Context, client client.Client) error {
414+
configMapResource := resource.NewConfigMap("my-configmap", "my-namespace", map[string]string{)
415+
"label1": "value1",
416+
"label2": "value2",
417+
}, nil)
418+
419+
serviceAccountResource := resource.NewServiceAccount("my-serviceaccount", "my-namespace", nil, nil)
420+
421+
myDeploymentMutator := newDeploymentMutator()
422+
423+
var err error
424+
425+
err = resources.CreateOrUpdateResource(ctx, client, configMapResource)
426+
if err != nil {
427+
return err
428+
}
429+
430+
resources.CreateOrUpdateResource(ctx, client, serviceAccountResource)
431+
if err != nil {
432+
return err
433+
}
434+
435+
err = resources.CreateOrUpdateResource(ctx, client, myDeploymentMutator)
436+
if err != nil {
437+
return err
438+
}
439+
440+
return nil
441+
}
442+
```
443+
328444
## Support, Feedback, Contributing
329445

330446
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/openmcp-project/controller-utils/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).

pkg/clusters/cluster.go

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ type Cluster struct {
2323
client client.Client
2424
// cluster
2525
cluster cluster.Cluster
26+
27+
clientOpts *client.Options
28+
clusterOpts []cluster.Option
2629
}
2730

2831
// Initializes a new cluster.
@@ -56,6 +59,36 @@ func (c *Cluster) RegisterConfigPathFlag(flags *flag.FlagSet) {
5659
flags.StringVar(&c.cfgPath, fmt.Sprintf("%s-cluster", c.id), "", fmt.Sprintf("Path to the %s cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.", c.id))
5760
}
5861

62+
// WithClientOptions allows to overwrite the default client options.
63+
// It must be called before InitializeClient().
64+
// Note that using this method disables the the scheme injection during client initialization.
65+
// This means that the required scheme should already be set in the options that are passed into this method.
66+
// Returns the cluster for chaining.
67+
func (c *Cluster) WithClientOptions(opts client.Options) *Cluster {
68+
c.clientOpts = &opts
69+
return c
70+
}
71+
72+
// WithClusterOptions allows to overwrite the default cluster options.
73+
// It must be called before InitializeClient().
74+
// Note that using this method disables the the scheme injection during client initialization.
75+
// This means that the required scheme should be set by the cluster options that are passed into this method.
76+
// The DefaultClusterOptions function can be passed in as a cluster option to set the scheme.
77+
// Returns the cluster for chaining.
78+
func (c *Cluster) WithClusterOptions(opts ...cluster.Option) *Cluster {
79+
c.clusterOpts = opts
80+
return c
81+
}
82+
83+
// DefaultClusterOptions returns the default cluster options.
84+
// This is useful when one wants to add custom cluster options without overwriting the default ones via WithClusterOptions().
85+
func DefaultClusterOptions(scheme *runtime.Scheme) cluster.Option {
86+
return func(o *cluster.Options) {
87+
o.Scheme = scheme
88+
o.Cache.Scheme = scheme
89+
}
90+
}
91+
5992
///////////////////
6093
// STATUS CHECKS //
6194
///////////////////
@@ -92,15 +125,11 @@ func (c *Cluster) InitializeID(id string) {
92125
}
93126

94127
// InitializeRESTConfig loads the cluster's REST config.
95-
// If the config has already been loaded, this is a no-op.
96128
// Panics if the cluster's id is not set (InitializeID must be called first).
97129
func (c *Cluster) InitializeRESTConfig() error {
98130
if !c.HasID() {
99131
panic("cluster id must be set before loading the config")
100132
}
101-
if c.HasRESTConfig() {
102-
return nil
103-
}
104133
cfg, err := controller.LoadKubeconfig(c.cfgPath)
105134
if err != nil {
106135
return fmt.Errorf("failed to load '%s' cluster kubeconfig: %w", c.ID(), err)
@@ -111,20 +140,29 @@ func (c *Cluster) InitializeRESTConfig() error {
111140

112141
// InitializeClient creates a new client for the cluster.
113142
// This also initializes the cluster's controller-runtime 'Cluster' representation.
114-
// If the client has already been initialized, this is a no-op.
115143
// Panics if the cluster's REST config has not been loaded (InitializeRESTConfig must be called first).
116144
func (c *Cluster) InitializeClient(scheme *runtime.Scheme) error {
117145
if !c.HasRESTConfig() {
118146
panic("cluster REST config must be set before creating the client")
119147
}
120-
if c.HasClient() {
121-
return nil
148+
if c.clientOpts == nil {
149+
c.clientOpts = &client.Options{
150+
Scheme: scheme,
151+
}
122152
}
123-
cli, err := client.New(c.restCfg, client.Options{Scheme: scheme})
153+
cli, err := client.New(c.restCfg, *c.clientOpts)
124154
if err != nil {
125155
return fmt.Errorf("failed to create '%s' cluster client: %w", c.ID(), err)
126156
}
127-
clu, err := cluster.New(c.restCfg, func(o *cluster.Options) { o.Scheme = scheme; o.Cache.Scheme = scheme })
157+
if c.clusterOpts == nil {
158+
c.clusterOpts = []cluster.Option{
159+
func(o *cluster.Options) {
160+
o.Scheme = scheme
161+
o.Cache.Scheme = scheme
162+
},
163+
}
164+
}
165+
clu, err := cluster.New(c.restCfg, c.clusterOpts...)
128166
if err != nil {
129167
return fmt.Errorf("failed to create '%s' cluster Cluster representation: %w", c.ID(), err)
130168
}

pkg/resources/clusterrole.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package resources
2+
3+
import (
4+
"fmt"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
8+
v1 "k8s.io/api/rbac/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
type ClusterRoleMutator struct {
13+
Name string
14+
Rules []v1.PolicyRule
15+
meta Mutator[client.Object]
16+
}
17+
18+
var _ Mutator[*v1.ClusterRole] = &ClusterRoleMutator{}
19+
20+
func NewClusterRoleMutator(name string, rules []v1.PolicyRule, labels map[string]string, annotations map[string]string) Mutator[*v1.ClusterRole] {
21+
return &ClusterRoleMutator{
22+
Name: name,
23+
Rules: rules,
24+
meta: NewMetadataMutator(labels, annotations),
25+
}
26+
}
27+
28+
func (m *ClusterRoleMutator) String() string {
29+
return fmt.Sprintf("clusterrole %s", m.Name)
30+
}
31+
32+
func (m *ClusterRoleMutator) Empty() *v1.ClusterRole {
33+
return &v1.ClusterRole{
34+
TypeMeta: metav1.TypeMeta{
35+
APIVersion: "rbac.authorization.k8s.io/v1",
36+
Kind: "ClusterRole",
37+
},
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: m.Name,
40+
},
41+
}
42+
}
43+
44+
func (m *ClusterRoleMutator) Mutate(r *v1.ClusterRole) error {
45+
r.Rules = m.Rules
46+
return m.meta.Mutate(r)
47+
}

0 commit comments

Comments
 (0)