Skip to content

Commit b68fa21

Browse files
committed
wire in bootstrap/config
1 parent 4c213d0 commit b68fa21

File tree

9 files changed

+2106
-27
lines changed

9 files changed

+2106
-27
lines changed

examples/kcp/Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,26 @@ build: $(KCP) $(KCP_APIGEN_GEN) $(CONTROLLER_GEN)
3737
.PHONY: kcp-server
3838
kcp-server: $(KCP) $(ARTIFACT_DIR)/kcp ## Run the kcp server.
3939
@if [[ ! -s $(ARTIFACT_DIR)/kcp.log ]]; then ( $(KCP) start -v 5 --root-directory $(ARTIFACT_DIR)/kcp --kubeconfig-path $(ARTIFACT_DIR)/kcp.kubeconfig --audit-log-maxsize 1024 --audit-log-mode=batch --audit-log-batch-max-wait=1s --audit-log-batch-max-size=1000 --audit-log-batch-buffer-size=10000 --audit-log-batch-throttle-burst=15 --audit-log-batch-throttle-enable=true --audit-log-batch-throttle-qps=10 --audit-policy-file ./test/e2e/audit-policy.yaml --audit-log-path $(ARTIFACT_DIR)/audit.log >$(ARTIFACT_DIR)/kcp.log 2>&1 & ); fi
40+
@echo "Waiting for kcp server to generate kubeconfig..."
4041
@while true; do if [[ ! -s $(ARTIFACT_DIR)/kcp.kubeconfig ]]; then sleep 0.2; else break; fi; done
42+
@echo "Waiting for kcp server to be ready..."
4143
@while true; do if ! kubectl --kubeconfig $(ARTIFACT_DIR)/kcp.kubeconfig get --raw /readyz >$(ARTIFACT_DIR)/kcp.probe.log 2>&1; then sleep 0.2; else break; fi; done
44+
@echo "kcp server is ready."
4245

4346
.PHONY: test-e2e-cleanup
4447
test-cleanup: ## Clean up processes and directories from an end-to-end test run.
4548
rm -rf $(ARTIFACT_DIR) || true
4649
pkill -sigterm kcp || true
4750
pkill -sigterm kubectl || true
4851

49-
ARTIFACT_DIR ?= .test
50-
5152
$(ARTIFACT_DIR)/kcp: ## Create a directory for the kcp server data.
5253
mkdir -p $(ARTIFACT_DIR)/kcp
5354

5455
generate: build
5556
./hack/update-codegen-crds.sh
57+
58+
59+
bootstrap:
60+
export KUBECONFIG=./.test/kcp.kubeconfig
61+
@go run ./config/main.go
62+
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package helpers
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"embed"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"strings"
12+
"text/template"
13+
"time"
14+
15+
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
16+
apierrors "k8s.io/apimachinery/pkg/api/errors"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/types"
19+
apimachineryerrors "k8s.io/apimachinery/pkg/util/errors"
20+
"k8s.io/apimachinery/pkg/util/sets"
21+
"k8s.io/apimachinery/pkg/util/wait"
22+
kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
23+
"sigs.k8s.io/controller-runtime/pkg/client"
24+
"sigs.k8s.io/controller-runtime/pkg/log"
25+
)
26+
27+
// Bootstrap creates resources in a package's fs by
28+
// continuously retrying the list. This is blocking, i.e. it only returns (with error)
29+
// when the context is closed or with nil when the bootstrapping is successfully completed.
30+
func Bootstrap(ctx context.Context, client client.Client, fs embed.FS, batteriesIncluded sets.Set[string]) error {
31+
// bootstrap non-crd resources
32+
return wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) {
33+
if err := CreateResourcesFromFS(ctx, client, fs, batteriesIncluded); err != nil {
34+
log.FromContext(ctx).WithValues("err", err).Info("failed to bootstrap resources, retrying")
35+
return false, nil
36+
}
37+
return true, nil
38+
})
39+
}
40+
41+
// CreateResourcesFromFS creates all resources from a filesystem.
42+
func CreateResourcesFromFS(ctx context.Context, client client.Client, fs embed.FS, batteriesIncluded sets.Set[string]) error {
43+
files, err := fs.ReadDir(".")
44+
if err != nil {
45+
return err
46+
}
47+
48+
var errs []error
49+
for _, f := range files {
50+
if f.IsDir() {
51+
continue
52+
}
53+
if err := CreateResourceFromFS(ctx, client, f.Name(), fs, batteriesIncluded); err != nil {
54+
errs = append(errs, err)
55+
}
56+
}
57+
return apimachineryerrors.NewAggregate(errs)
58+
}
59+
60+
// CreateResourceFromFS creates given resource file.
61+
func CreateResourceFromFS(ctx context.Context, client client.Client, filename string, fs embed.FS, batteriesIncluded sets.Set[string]) error {
62+
raw, err := fs.ReadFile(filename)
63+
if err != nil {
64+
return fmt.Errorf("could not read %s: %w", filename, err)
65+
}
66+
67+
if len(raw) == 0 {
68+
return nil // ignore empty files
69+
}
70+
71+
d := kubeyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(raw)))
72+
var errs []error
73+
for i := 1; ; i++ {
74+
doc, err := d.Read()
75+
if errors.Is(err, io.EOF) {
76+
break
77+
} else if err != nil {
78+
return err
79+
}
80+
if len(bytes.TrimSpace(doc)) == 0 {
81+
continue
82+
}
83+
84+
if err := createResourceFromFS(ctx, client, doc, batteriesIncluded); err != nil {
85+
errs = append(errs, fmt.Errorf("failed to create resource %s doc %d: %w", filename, i, err))
86+
}
87+
}
88+
return apimachineryerrors.NewAggregate(errs)
89+
}
90+
91+
const annotationCreateOnlyKey = "bootstrap.kcp.io/create-only"
92+
const annotationBattery = "bootstrap.kcp.io/battery"
93+
94+
func createResourceFromFS(ctx context.Context, client client.Client, raw []byte, batteriesIncluded sets.Set[string]) error {
95+
log := log.FromContext(ctx)
96+
97+
type Input struct {
98+
Batteries map[string]bool
99+
}
100+
input := Input{
101+
Batteries: map[string]bool{},
102+
}
103+
for _, b := range sets.List[string](batteriesIncluded) {
104+
input.Batteries[b] = true
105+
}
106+
tmpl, err := template.New("manifest").Parse(string(raw))
107+
if err != nil {
108+
return fmt.Errorf("failed to parse manifest: %w", err)
109+
}
110+
var buf bytes.Buffer
111+
if err := tmpl.Execute(&buf, input); err != nil {
112+
return fmt.Errorf("failed to execute manifest: %w", err)
113+
}
114+
115+
obj, gvk, err := extensionsapiserver.Codecs.UniversalDeserializer().Decode(buf.Bytes(), nil, &unstructured.Unstructured{})
116+
if err != nil {
117+
return fmt.Errorf("could not decode raw: %w", err)
118+
}
119+
u, ok := obj.(*unstructured.Unstructured)
120+
if !ok {
121+
return fmt.Errorf("decoded into incorrect type, got %T, wanted %T", obj, &unstructured.Unstructured{})
122+
}
123+
124+
if v, found := u.GetAnnotations()[annotationBattery]; found {
125+
partOf := strings.Split(v, ",")
126+
included := false
127+
for _, p := range partOf {
128+
if batteriesIncluded.Has(strings.TrimSpace(p)) {
129+
included = true
130+
break
131+
}
132+
}
133+
if !included {
134+
log.V(4).WithValues("resource", u.GetName(), "batteriesRequired", v, "batteriesIncluded", batteriesIncluded).Info("skipping resource because required batteries are not among included batteries")
135+
return nil
136+
}
137+
}
138+
139+
key := types.NamespacedName{
140+
Namespace: u.GetNamespace(),
141+
Name: u.GetName(),
142+
}
143+
err = client.Create(ctx, u)
144+
if err != nil {
145+
if apierrors.IsAlreadyExists(err) {
146+
err = client.Get(ctx, key, u)
147+
if err != nil {
148+
return err
149+
}
150+
151+
if _, exists := u.GetAnnotations()[annotationCreateOnlyKey]; exists {
152+
log.Info("skipping update of object because it has the create-only annotation")
153+
154+
return nil
155+
}
156+
157+
u.SetResourceVersion(u.GetResourceVersion())
158+
err = client.Update(ctx, u)
159+
if err != nil {
160+
return fmt.Errorf("could not update %s %s: %w", gvk.Kind, key.String(), err)
161+
} else {
162+
log.WithValues("resource", u.GetName(), "kind", gvk.Kind).Info("updated object")
163+
return nil
164+
}
165+
}
166+
return err
167+
}
168+
169+
log.WithValues("resource", u.GetName(), "kind", gvk.Kind).Info("created object")
170+
171+
return nil
172+
}

examples/kcp/config/main.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
Copyright 2024 The KCP 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 main
18+
19+
import (
20+
"net/url"
21+
22+
"github.com/davecgh/go-spew/spew"
23+
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
24+
corev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1"
25+
tenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
28+
"k8s.io/apimachinery/pkg/util/sets"
29+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
"sigs.k8s.io/controller-runtime/pkg/client"
32+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
33+
34+
widgets "github.com/kcp-dev/controller-runtime/examples/kcp/config/widgets"
35+
"github.com/kcp-dev/controller-runtime/examples/kcp/config/widgets/resources"
36+
"sigs.k8s.io/controller-runtime/pkg/log"
37+
)
38+
39+
// config is bootstrap set of assets for the controller-runtime examples.
40+
// It includes the following assets:
41+
// - crds/* for the Widget type - autogenerated from the Widget type definition
42+
// - widgets/resources/* - a set of Widget resources for KCP to manage. Automatically generated by kcp apigen
43+
// see Makefile & hack/update-codegen-crds.sh for more details
44+
45+
// It is intended to be running with higher privileges than the examples themselves
46+
// to ensure system (kcp) is bootstrapped. In real world scenarios, this would be
47+
// done by the platform operator to enable service providers to deploy their
48+
// controllers.
49+
50+
var (
51+
scheme = runtime.NewScheme()
52+
)
53+
54+
func init() {
55+
utilruntime.Must(tenancyv1alpha1.AddToScheme(scheme))
56+
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
57+
utilruntime.Must(corev1alpha1.AddToScheme(scheme))
58+
utilruntime.Must(apisv1alpha1.AddToScheme(scheme))
59+
60+
}
61+
62+
var (
63+
// clusterName is the workspace to host common APIs.
64+
clusterName = "widgets"
65+
)
66+
67+
func main() {
68+
opts := zap.Options{
69+
Development: true,
70+
}
71+
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
72+
73+
ctx := ctrl.SetupSignalHandler()
74+
restConfig := ctrl.GetConfigOrDie()
75+
76+
log := log.FromContext(ctx)
77+
78+
c, err := client.New(restConfig, client.Options{
79+
Scheme: scheme,
80+
})
81+
if err != nil {
82+
log.Error(err, "unable to create client")
83+
}
84+
fakeBatteries := sets.New("")
85+
86+
err = widgets.Bootstrap(ctx, c, fakeBatteries)
87+
if err != nil {
88+
log.Error(err, "failed to bootstrap widgets")
89+
}
90+
91+
// Hack to set the clusterName in the restConfig.Host
92+
restConfig.Host = restConfig.Host + ":" + url.PathEscape(clusterName)
93+
if err != nil {
94+
log.Error(err, "unable to set clusterName")
95+
}
96+
spew.Dump(restConfig.Host)
97+
c, err = client.New(restConfig, client.Options{
98+
Scheme: scheme,
99+
})
100+
if err != nil {
101+
log.Error(err, "unable to create client")
102+
}
103+
104+
err = resources.Bootstrap(ctx, c, fakeBatteries)
105+
if err != nil {
106+
log.Error(err, "failed to bootstrap resources")
107+
}
108+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2024 The KCP 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 widdgets
18+
19+
import (
20+
"context"
21+
"embed"
22+
23+
"k8s.io/apimachinery/pkg/util/sets"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
25+
"sigs.k8s.io/controller-runtime/pkg/log"
26+
27+
confighelpers "github.com/kcp-dev/controller-runtime/examples/kcp/config/helpers"
28+
)
29+
30+
//go:embed *.yaml
31+
var fs embed.FS
32+
33+
// Bootstrap creates resources in this package by continuously retrying the list.
34+
// This is blocking, i.e. it only returns (with error) when the context is closed or with nil when
35+
// the bootstrapping is successfully completed.
36+
func Bootstrap(
37+
ctx context.Context,
38+
client client.Client,
39+
batteriesIncluded sets.Set[string],
40+
) error {
41+
log := log.FromContext(ctx)
42+
43+
log.Info("Bootstrapping widgets workspace")
44+
return confighelpers.Bootstrap(ctx, client, fs, batteriesIncluded)
45+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apiVersion: tenancy.kcp.io/v1alpha1
2+
kind: Workspace
3+
metadata:
4+
name: widgets
5+
annotations:
6+
bootstrap.kcp.io/create-only: "true"
7+
spec:
8+
type:
9+
name: universal
10+
path: root
11+
location:
12+
selector:
13+
matchLabels:
14+
name: root

0 commit comments

Comments
 (0)