Skip to content

Commit 1fca459

Browse files
committed
Bootstrap root workspace with pre-defined identities
Adds a new --root-identities-file kcp cmd flag to read the APIExport-identity pairs from a file. On-behalf-of: @SAP [email protected] Signed-off-by: Robert Vasek <[email protected]>
1 parent 4628ed5 commit 1fca459

File tree

6 files changed

+164
-0
lines changed

6 files changed

+164
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2025 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 rootidentities
18+
19+
import (
20+
"context"
21+
"embed"
22+
"fmt"
23+
24+
"sigs.k8s.io/yaml"
25+
26+
"k8s.io/client-go/discovery"
27+
"k8s.io/client-go/dynamic"
28+
"k8s.io/client-go/kubernetes"
29+
30+
confighelpers "github.com/kcp-dev/kcp/config/helpers"
31+
)
32+
33+
//go:embed *.yaml
34+
var fs embed.FS
35+
36+
// bootstrapIdentities is the structure we expect to receive
37+
// from --root-identities-file in YAML format.
38+
type bootstrapIdentities struct {
39+
Identities []struct {
40+
Export string `json:"export"`
41+
Identity string `json:"identity"`
42+
} `json:"identities"`
43+
}
44+
45+
// Bootstrap creates resources in this package by continuously retrying the list.
46+
// This is blocking, i.e. it only returns (with error) when the context is closed or with nil when
47+
// the bootstrapping is successfully completed.
48+
func Bootstrap(
49+
ctx context.Context,
50+
rootDiscoveryClient discovery.DiscoveryInterface,
51+
rootDynamicClient dynamic.Interface,
52+
kubeClient kubernetes.Interface,
53+
bootstrapIdentitiesBytes []byte,
54+
) error {
55+
var identities bootstrapIdentities
56+
if err := yaml.UnmarshalStrict(bootstrapIdentitiesBytes, &identities); err != nil {
57+
return fmt.Errorf("failed to parse bootstrap identities: %v", err)
58+
}
59+
60+
for _, identity := range identities.Identities {
61+
err := confighelpers.Bootstrap(ctx, kubeClient.Discovery(), rootDynamicClient, nil, fs, confighelpers.ReplaceOption(
62+
"IDENTITY_APIEXPORT", identity.Export,
63+
"IDENTITY_KEY", identity.Identity,
64+
))
65+
if err != nil {
66+
return fmt.Errorf("failed to bootstrap identity secret for %s: %v", identity.Export, err)
67+
}
68+
}
69+
70+
return nil
71+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 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 namespace
18+
19+
import (
20+
"context"
21+
"embed"
22+
23+
"k8s.io/client-go/discovery"
24+
"k8s.io/client-go/dynamic"
25+
"k8s.io/client-go/kubernetes"
26+
27+
confighelpers "github.com/kcp-dev/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+
rootDiscoveryClient discovery.DiscoveryInterface,
39+
rootDynamicClient dynamic.Interface,
40+
kubeClient kubernetes.Interface,
41+
) error {
42+
return confighelpers.Bootstrap(ctx, kubeClient.Discovery(), rootDynamicClient, nil, fs)
43+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: v1
2+
kind: Namespace
3+
metadata:
4+
name: kcp-system
5+
annotations:
6+
bootstrap.kcp.io/create-only: "true"
7+
kcp.io/cluster: root
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: IDENTITY_APIEXPORT
5+
namespace: kcp-system
6+
annotations:
7+
bootstrap.kcp.io/create-only: "true"
8+
stringData:
9+
key: IDENTITY_KEY

pkg/server/options/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type ExtraOptions struct {
6767
LogicalClusterAdminKubeconfig string
6868
ExternalLogicalClusterAdminKubeconfig string
6969
ConversionCELTransformationTimeout time.Duration
70+
RootIdentitiesFile string
7071
BatteriesIncluded []string
7172
// DEVELOPMENT ONLY. AdditionalMappingsFile is the path to a file that contains additional mappings
7273
// for the mini-front-proxy to use. The file should be in the format of the
@@ -166,6 +167,7 @@ func (o *Options) AddFlags(fss *cliflag.NamedFlagSets) {
166167
fs.StringVar(&o.Extra.ShardClientKeyFile, "shard-client-key-file", o.Extra.ShardClientKeyFile, "Path to a client certificate key file the shard uses to communicate with other system components.")
167168
fs.StringVar(&o.Extra.LogicalClusterAdminKubeconfig, "logical-cluster-admin-kubeconfig", o.Extra.LogicalClusterAdminKubeconfig, "Kubeconfig holding system:kcp:logical-cluster-admin credentials for connecting to other shards. Defaults to the loopback client")
168169
fs.StringVar(&o.Extra.ExternalLogicalClusterAdminKubeconfig, "external-logical-cluster-admin-kubeconfig", o.Extra.ExternalLogicalClusterAdminKubeconfig, "Kubeconfig holding system:kcp:external-logical-cluster-admin credentials for connecting to the external address (e.g. the front-proxy). Defaults to the loopback client")
170+
fs.StringVar(&o.Extra.RootIdentitiesFile, "root-identities-file", "", "Path to a YAML file used to bootstrap APIExport identities inside the root workspace. The YAML file must be structured as {\"identities\": [ {\"export\": \"<APIExport name>\", \"identity\": \"<APIExport identity>\"}, ... ]}. If a secret with matching APIExport name already exists inside kcp-system namespace, it will be left unchanged. Defaults to empty, i.e. no identities are bootstrapped.")
169171

170172
fs.BoolVar(&o.Extra.ExperimentalBindFreePort, "experimental-bind-free-port", o.Extra.ExperimentalBindFreePort, "Bind to a free port. --secure-port must be 0. Use the admin.kubeconfig to extract the chosen port.")
171173
fs.MarkHidden("experimental-bind-free-port") //nolint:errcheck

pkg/server/server.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import (
4747
"github.com/kcp-dev/logicalcluster/v3"
4848

4949
configroot "github.com/kcp-dev/kcp/config/root"
50+
configrootidentities "github.com/kcp-dev/kcp/config/root-identities"
51+
configrootidentitiesns "github.com/kcp-dev/kcp/config/root-identities/namespace"
5052
configrootphase0 "github.com/kcp-dev/kcp/config/root-phase0"
5153
configshard "github.com/kcp-dev/kcp/config/shard"
5254
systemcrds "github.com/kcp-dev/kcp/config/system-crds"
@@ -434,6 +436,36 @@ func (s *Server) Run(ctx context.Context) error {
434436
logger.Info("bootstrapping root workspace phase 0")
435437
s.RootShardKcpClusterClient = s.KcpClusterClient
436438

439+
if s.Options.Extra.RootIdentitiesFile != "" {
440+
logger.Info("bootstrapping identities into root workspace", "file", s.Options.Extra.RootIdentitiesFile)
441+
logger.Info("creating system namespace in root workspace")
442+
if err := configrootidentitiesns.Bootstrap(hookCtx,
443+
s.BootstrapApiExtensionsClusterClient.Cluster(core.RootCluster.Path()).Discovery(),
444+
s.DynamicClusterClient.Cluster(core.RootCluster.Path()),
445+
s.KubeClusterClient.Cluster(core.RootCluster.Path()),
446+
); err != nil {
447+
logger.Error(err, "failed to bootstrap identity secrets namespace")
448+
return nil // don't klog.Fatal. This only happens when context is cancelled.
449+
}
450+
logger.Info("created system namespace in root workspace")
451+
logger.Info("bootstrapping root workspace with identities", "file", s.Options.Extra.RootIdentitiesFile)
452+
identitiesBytes, err := os.ReadFile(s.Options.Extra.RootIdentitiesFile)
453+
if err != nil {
454+
logger.Error(err, "failed to read root identities file")
455+
return nil // don't klog.Fatal. This only happens when context is cancelled.
456+
}
457+
if err := configrootidentities.Bootstrap(hookCtx,
458+
s.BootstrapApiExtensionsClusterClient.Cluster(core.RootCluster.Path()).Discovery(),
459+
s.DynamicClusterClient.Cluster(core.RootCluster.Path()),
460+
s.KubeClusterClient.Cluster(core.RootCluster.Path()),
461+
identitiesBytes,
462+
); err != nil {
463+
logger.Error(err, "failed to bootstrap root workspace with identities")
464+
return nil // don't klog.Fatal. This only happens when context is cancelled.
465+
}
466+
logger.Info("bootstrapped root workspace with identities", "file", s.Options.Extra.RootIdentitiesFile)
467+
}
468+
437469
// bootstrap root workspace phase 0 only if we are on the root shard, no APIBinding resources yet
438470
if err := configrootphase0.Bootstrap(hookCtx,
439471
s.KcpClusterClient.Cluster(core.RootCluster.Path()),

0 commit comments

Comments
 (0)