Skip to content

Commit b6e824c

Browse files
Implement library for topology mutation hooks implementation
1 parent 2122310 commit b6e824c

File tree

23 files changed

+1468
-557
lines changed

23 files changed

+1468
-557
lines changed

docs/book/src/tasks/experimental-features/runtime-sdk/implement-extensions.md

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ import (
5656
)
5757

5858
var (
59-
catalog = runtimecatalog.New()
60-
setupLog = ctrl.Log.WithName("setup")
59+
// catalog contains all information about RuntimeHooks.
60+
catalog = runtimecatalog.New()
6161

6262
// Flags.
6363
profilerAddress string
@@ -67,15 +67,17 @@ var (
6767
)
6868

6969
func init() {
70-
// Register the Runtime Hook types into the catalog.
70+
// Adds to the catalog all the RuntimeHooks defined in cluster API.
7171
_ = runtimehooksv1.AddToCatalog(catalog)
7272
}
7373

7474
// InitFlags initializes the flags.
7575
func InitFlags(fs *pflag.FlagSet) {
76+
// Initialize logs flags using Kubernetes component-base machinery.
7677
logs.AddFlags(fs, logs.SkipLoggingConfigurationFlags())
7778
logOptions.AddFlags(fs)
7879

80+
// Add test-extension specific flags
7981
fs.StringVar(&profilerAddress, "profiler-address", "",
8082
"Bind address to expose the pprof profiler (e.g. localhost:6060)")
8183

@@ -87,29 +89,34 @@ func InitFlags(fs *pflag.FlagSet) {
8789
}
8890

8991
func main() {
92+
// Creates a logger to be used during the main func.
93+
setupLog := ctrl.Log.WithName("main")
94+
95+
// Initialize and parse command line flags.
9096
InitFlags(pflag.CommandLine)
9197
pflag.CommandLine.SetNormalizeFunc(cliflag.WordSepNormalizeFunc)
9298
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
9399
pflag.Parse()
94100

101+
// Validates logs flags using Kubernetes component-base machinery and applies them
95102
if err := logOptions.ValidateAndApply(nil); err != nil {
96103
setupLog.Error(err, "unable to start extension")
97104
os.Exit(1)
98105
}
99106

100-
// klog.Background will automatically use the right logger.
107+
// Add the klog logger in the context.
101108
ctrl.SetLogger(klog.Background())
102109

110+
// Initialize the golang profiler server, if required.
103111
if profilerAddress != "" {
104112
klog.Infof("Profiler listening for requests at %s", profilerAddress)
105113
go func() {
106114
klog.Info(http.ListenAndServe(profilerAddress, nil))
107115
}()
108116
}
109117

110-
ctx := ctrl.SetupSignalHandler()
111-
112-
webhookServer, err := server.NewServer(server.Options{
118+
// Create a http server for serving runtime extensions
119+
webhookServer, err := server.New(server.Options{
113120
Catalog: catalog,
114121
Port: webhookPort,
115122
CertDir: webhookCertDir,
@@ -124,8 +131,6 @@ func main() {
124131
Hook: runtimehooksv1.BeforeClusterCreate,
125132
Name: "before-cluster-create",
126133
HandlerFunc: DoBeforeClusterCreate,
127-
TimeoutSeconds: pointer.Int32(5),
128-
FailurePolicy: toPtr(runtimehooksv1.FailurePolicyFail),
129134
}); err != nil {
130135
setupLog.Error(err, "error adding handler")
131136
os.Exit(1)
@@ -134,13 +139,15 @@ func main() {
134139
Hook: runtimehooksv1.BeforeClusterUpgrade,
135140
Name: "before-cluster-upgrade",
136141
HandlerFunc: DoBeforeClusterUpgrade,
137-
TimeoutSeconds: pointer.Int32(5),
138-
FailurePolicy: toPtr(runtimehooksv1.FailurePolicyFail),
139142
}); err != nil {
140143
setupLog.Error(err, "error adding handler")
141144
os.Exit(1)
142145
}
143146

147+
// Setup a context listening for SIGINT.
148+
ctx := ctrl.SetupSignalHandler()
149+
150+
// Start the https server.
144151
setupLog.Info("Starting Runtime Extension server")
145152
if err := webhookServer.Start(ctx); err != nil {
146153
setupLog.Error(err, "error running webhook server")
@@ -159,10 +166,6 @@ func DoBeforeClusterUpgrade(ctx context.Context, request *runtimehooksv1.BeforeC
159166
log.Info("BeforeClusterUpgrade is called")
160167
// Your implementation
161168
}
162-
163-
func toPtr(f runtimehooksv1.FailurePolicy) *runtimehooksv1.FailurePolicy {
164-
return &f
165-
}
166169
```
167170

168171
For a full example see our [test extension](https://github.com/kubernetes-sigs/cluster-api/tree/main/test/extension).
@@ -270,7 +273,26 @@ Some Runtime Hooks, e.g. like external patches, might explicitly request for cor
270273
to support this property. But we encourage developers to follow this pattern more generally given that it fits
271274
well with practices like unit testing and generally makes the entire system more predictable and easier to troubleshoot.
272275

273-
### Error Management
276+
### Error messages
277+
278+
RuntimeExtension authors should be aware that error messages are surfaced as a conditions in Kubernetes resources
279+
and recorded in Cluster API controller's logs. As a consequence:
280+
281+
- Error message must not contain any sensitive information.
282+
- Error message must be deterministic, and must avoid to including timestamps or values changing at every call.
283+
- Error message must not contain external errors when it's not clear if those errors are deterministic (e.g. errors return from cloud APIs).
284+
285+
<aside class="note warning">
286+
287+
<h1>Caution</h1>
288+
289+
If an error message is not deterministic and it changes at every call even if the problem is the same, it could
290+
lead to to Kubernetes resources conditions continuously changing, and this generates a denial attack to
291+
controllers processing those resource that might impact system stability.
292+
293+
</aside>
294+
295+
### Error management
274296

275297
In case a Runtime Extension returns an error, the error will be handled according to the corresponding failure policy
276298
defined in the response of the Discovery call.

docs/book/src/tasks/experimental-features/runtime-sdk/implement-lifecycle-hooks.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ potentially block lifecycle transitions from happening.
2929
Following recommendations are especially relevant:
3030

3131
* [Blocking and non Blocking](implement-extensions.md#blocking-hooks)
32+
* [Error messages](implement-extensions.md#error-messages)
3233
* [Error management](implement-extensions.md#error-management)
3334
* [Avoid dependencies](implement-extensions.md#avoid-dependencies)
3435

docs/book/src/tasks/experimental-features/runtime-sdk/implement-topology-mutation-hook.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ This section outlines considerations specific to Topology Mutation hooks:
6767
i.e. unnecessary patches when the template is already in the desired state must be avoided.
6868
* **Avoid Dependencies**: An External Patch Extension must be independent of other External Patch Extensions. However
6969
if dependencies cannot be avoided, it is possible to control the order in which patches are executed via the ClusterClass.
70+
* **Error messages**: For a given request (a set of templates and variables) an External Patch Extension must
71+
always return the same error message. Otherwise the system might became unstable due to controllers being overloaded
72+
by continuous changes to Kubernetes resources as these messages are reported as conditions. See [error messages](implement-extensions.md#error-messages).
7073

7174
## Definitions
7275

exp/runtime/hooks/api/v1alpha1/discovery_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
2323
)
2424

25+
// DefaultHandlersTimeoutSeconds defines the default timeout duration for client calls to ExtensionHandlers.
26+
const DefaultHandlersTimeoutSeconds = 10
27+
2528
// DiscoveryRequest is the request of the Discovery hook.
2629
// +kubebuilder:object:root=true
2730
type DiscoveryRequest struct {

exp/runtime/server/server.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,14 @@ type Options struct {
7171
}
7272

7373
// NewServer creates a new runtime webhook server based on the given Options.
74+
//
75+
// Deprecated: use New instead.
7476
func NewServer(options Options) (*Server, error) {
77+
return New(options)
78+
}
79+
80+
// New creates a new runtime webhook server based on the given Options.
81+
func New(options Options) (*Server, error) {
7582
if options.Catalog == nil {
7683
return nil, errors.Errorf("catalog is required")
7784
}
@@ -118,9 +125,13 @@ type ExtensionHandler struct {
118125
HandlerFunc runtimecatalog.Hook
119126

120127
// TimeoutSeconds is the timeout of the extension handler.
128+
// If left undefined, this will be defaulted to 10s when processing the answer to the discovery
129+
// call for this server.
121130
TimeoutSeconds *int32
122131

123-
// FailurePolicy is the failure policy of the extension handler
132+
// FailurePolicy is the failure policy of the extension handler.
133+
// If left undefined, this will be defaulted to FailurePolicyFail when processing the answer to the discovery
134+
// call for this server.
124135
FailurePolicy *runtimehooksv1.FailurePolicy
125136
}
126137

exp/runtime/topologymutation/doc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
Copyright 2022 The Kubernetes 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 topologymutation provides helpers for implementing the topology mutation hooks.
18+
package topologymutation
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2022 The Kubernetes 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 topologymutation
18+
19+
import "strings"
20+
21+
// logRef is used to correctly render a reference with GroupVersionKind, Namespace and Name for both JSON and text logging.
22+
type logRef struct {
23+
Group string `json:"group,omitempty"`
24+
Version string `json:"version,omitempty"`
25+
Kind string `json:"kind,omitempty"`
26+
Namespace string `json:"namespace,omitempty"`
27+
Name string `json:"name,omitempty"`
28+
}
29+
30+
func (l logRef) String() string {
31+
var parts []string
32+
for _, s := range []string{l.Group, l.Version, l.Kind, l.Namespace, l.Name} {
33+
if strings.TrimSpace(s) != "" {
34+
parts = append(parts, s)
35+
}
36+
}
37+
return strings.Join(parts, "/")
38+
}
39+
40+
type logRefWithoutStringFunc logRef
41+
42+
// MarshalLog ensures that loggers with support for structured output will log
43+
// as a struct by removing the String method via a custom type.
44+
func (l logRef) MarshalLog() interface{} {
45+
return logRefWithoutStringFunc(l)
46+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2022 The Kubernetes 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 topologymutation
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
)
24+
25+
func Test_Log(t *testing.T) {
26+
g := NewWithT(t)
27+
28+
l := &logRef{
29+
Group: "group",
30+
Version: "version",
31+
Kind: "kind",
32+
Namespace: "namespace",
33+
Name: "name",
34+
}
35+
36+
g.Expect(l.String()).To(Equal("group/version/kind/namespace/name"))
37+
g.Expect(l.MarshalLog()).To(Equal(logRefWithoutStringFunc(*l)))
38+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
Copyright 2022 The Kubernetes 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 topologymutation
18+
19+
import (
20+
"strconv"
21+
22+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
23+
24+
patchvariables "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/patches/variables"
25+
)
26+
27+
// TODO: Add func for validating received variables are of the expected types
28+
// TODO: Add func for converting complex variables into go structs and/or other basic types.
29+
30+
// GetVariable get the variable value.
31+
func GetVariable(templateVariables map[string]apiextensionsv1.JSON, variableName string) (*apiextensionsv1.JSON, bool, error) {
32+
value, err := patchvariables.GetVariableValue(templateVariables, variableName)
33+
if err != nil {
34+
if patchvariables.IsNotFoundError(err) {
35+
return nil, false, nil
36+
}
37+
return nil, false, err
38+
}
39+
return value, true, nil
40+
}
41+
42+
// GetStringVariable get the variable value as a string.
43+
func GetStringVariable(templateVariables map[string]apiextensionsv1.JSON, variableName string) (string, bool, error) {
44+
value, found, err := GetVariable(templateVariables, variableName)
45+
if !found || err != nil {
46+
return "", found, err
47+
}
48+
49+
// Unquote the JSON string.
50+
stringValue, err := strconv.Unquote(string(value.Raw))
51+
if err != nil {
52+
return "", true, err
53+
}
54+
return stringValue, true, nil
55+
}

0 commit comments

Comments
 (0)