Skip to content

Commit f797aa6

Browse files
committed
feat: generate talosconfig as a secret with proper endpoints
Fixes #83 fixes #85 Generate cluster-wide `talosconfig` in a secret `<cluster>-talosconfig`. Reconcile controlplane machine addresses and put them as `endpoints:` in the generated `talosconfig`. Other fixes: * use proper method to generate `talosconfig` * generate regular `talosconfig` in Status for user-supplied machine config if possible * handle pause for cluster reconcilation Signed-off-by: Andrey Smirnov <[email protected]>
1 parent 7541eba commit f797aa6

File tree

11 files changed

+341
-63
lines changed

11 files changed

+341
-63
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ REGISTRY_AND_USERNAME := $(REGISTRY)/$(USERNAME)
77
NAME := cluster-api-talos-controller
88

99
ARTIFACTS := _out
10+
TEST_RUN ?= ./...
1011

1112
TOOLS ?= ghcr.io/talos-systems/tools:v0.8.0-alpha.0-3-g2790b55
1213
PKGS ?= v0.8.0
@@ -130,7 +131,7 @@ conformance: ## Performs policy checks against the commit and source code.
130131
# Make `make test` behave just like `go test` regarding relative paths.
131132
test: ## Run tests.
132133
@$(MAKE) local-integration-test DEST=./internal/integration PLATFORM=linux/amd64
133-
cd internal/integration && KUBECONFIG=../../kubeconfig ./integration.test -test.v -test.coverprofile=../../coverage.txt
134+
cd internal/integration && KUBECONFIG=../../kubeconfig ./integration.test -test.v -test.coverprofile=../../coverage.txt -test.run $(TEST_RUN)
134135

135136
coverage: ## Upload coverage data to codecov.io.
136137
/usr/local/bin/codecov -f coverage.txt -X fix

api/v1alpha3/conditions.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,13 @@ const (
3030
// and user intervention is required to get them fixed.
3131
DataSecretGenerationFailedReason = "DataSecretGenerationFailed"
3232
)
33+
34+
const (
35+
// ClientConfigAvailableCondition documents the status of the client config generation process.
36+
ClientConfigAvailableCondition capiv1.ConditionType = "ClientConfigAvailable"
37+
38+
// ClientConfigGenerationFailedReason (Severity=Warning) documents a TalosConfig controller detecting
39+
// an error while generating a client config; those kind of errors are usually due to misconfigurations
40+
// and user intervention is required to get them fixed.
41+
ClientConfigGenerationFailedReason = "ClientConfigGenerationFailed"
42+
)

api/v1alpha3/talosconfig_types.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ type TalosConfigStatus struct {
3232
// +optional
3333
DataSecretName *string `json:"dataSecretName,omitempty"`
3434

35-
// Talos config will be a string containing the config for download
35+
// Talos config will be a string containing the config for download.
36+
//
37+
// Deprecated: please use `<cluster>-talosconfig` secret.
38+
//
3639
// +optional
3740
TalosConfig string `json:"talosConfig,omitempty"`
3841

controllers/secrets.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ package controllers
77
import (
88
"context"
99
"fmt"
10+
"sort"
1011

12+
"github.com/go-logr/logr"
1113
"github.com/talos-systems/crypto/x509"
1214
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate"
15+
talosmachine "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
1316
"gopkg.in/yaml.v2"
1417
corev1 "k8s.io/api/core/v1"
1518
k8serrors "k8s.io/apimachinery/pkg/api/errors"
1619
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1720
capiv1 "sigs.k8s.io/cluster-api/api/v1alpha4"
21+
"sigs.k8s.io/cluster-api/util/collections"
1822
"sigs.k8s.io/controller-runtime/pkg/client"
1923

2024
bootstrapv1alpha3 "github.com/talos-systems/cluster-api-bootstrap-provider-talos/api/v1alpha3"
@@ -35,14 +39,20 @@ func (r *TalosConfigReconciler) fetchSecret(ctx context.Context, config *bootstr
3539
}
3640

3741
// getSecretsBundle either generates or loads existing secret.
38-
func (r *TalosConfigReconciler) getSecretsBundle(ctx context.Context, scope *TalosConfigScope, secretName string, opts ...generate.GenOption) (*generate.SecretsBundle, error) {
42+
func (r *TalosConfigReconciler) getSecretsBundle(ctx context.Context, scope *TalosConfigScope, allowGenerate bool, opts ...generate.GenOption) (*generate.SecretsBundle, error) {
3943
var secretsBundle *generate.SecretsBundle
4044

45+
secretName := scope.Cluster.Name + "-talos"
46+
4147
retry:
4248
secret, err := r.fetchSecret(ctx, scope.Config, secretName)
4349

4450
switch {
4551
case err != nil && k8serrors.IsNotFound(err):
52+
if !allowGenerate {
53+
return nil, fmt.Errorf("secrets bundle is missing")
54+
}
55+
4656
// no cluster secret yet, generate new one
4757
secretsBundle, err = generate.NewSecretsBundle(generate.NewClock(), opts...)
4858
if err != nil {
@@ -190,3 +200,80 @@ func (r *TalosConfigReconciler) writeBootstrapData(ctx context.Context, scope *T
190200

191201
return dataSecretName, err
192202
}
203+
204+
// reconcileClientConfig creates/updates a TalosConfig for the cluster.
205+
func (r *TalosConfigReconciler) reconcileClientConfig(ctx context.Context, log logr.Logger, scope *TalosConfigScope) error {
206+
if !(scope.Config.Spec.GenerateType == talosmachine.TypeControlPlane.String() || scope.Config.Spec.GenerateType == talosmachine.TypeInit.String()) {
207+
// can only reconcile for control plane machines
208+
return nil
209+
}
210+
211+
machines, err := collections.GetFilteredMachinesForCluster(ctx, r.Client, scope.Cluster, collections.ControlPlaneMachines(scope.Cluster.Name))
212+
if err != nil {
213+
return fmt.Errorf("failed getting control plane machines: %w", err)
214+
}
215+
216+
var endpoints []string
217+
218+
for _, machine := range machines {
219+
for _, addr := range machine.Status.Addresses {
220+
if addr.Type == capiv1.MachineExternalIP || addr.Type == capiv1.MachineInternalIP {
221+
endpoints = append(endpoints, addr.Address)
222+
}
223+
}
224+
}
225+
226+
sort.Strings(endpoints)
227+
228+
secretBundle, err := r.getSecretsBundle(ctx, scope, false)
229+
if err != nil {
230+
return err
231+
}
232+
233+
talosConfig, err := genTalosConfigFile(scope.Cluster.Name, secretBundle, endpoints)
234+
if err != nil {
235+
return err
236+
}
237+
238+
// Create or update talosconfig secret
239+
dataSecretName := scope.Cluster.GetName() + "-talosconfig"
240+
241+
log.Info("updating talosconfig", "endpoints", endpoints, "secret", dataSecretName)
242+
243+
configSecret := &corev1.Secret{}
244+
245+
err = r.Client.Get(ctx, client.ObjectKey{Namespace: scope.Cluster.Namespace, Name: dataSecretName}, configSecret)
246+
if err != nil {
247+
if !k8serrors.IsNotFound(err) {
248+
return fmt.Errorf("error fetching secret: %w", err)
249+
}
250+
251+
configSecret = &corev1.Secret{
252+
ObjectMeta: metav1.ObjectMeta{
253+
Namespace: scope.Cluster.Namespace,
254+
Name: dataSecretName,
255+
Labels: map[string]string{
256+
capiv1.ClusterLabelName: scope.Cluster.Name,
257+
},
258+
OwnerReferences: []metav1.OwnerReference{
259+
*metav1.NewControllerRef(scope.Cluster, capiv1.GroupVersion.WithKind("Cluster")),
260+
},
261+
},
262+
Data: map[string][]byte{
263+
"talosconfig": []byte(talosConfig),
264+
},
265+
}
266+
267+
return r.Client.Create(ctx, configSecret)
268+
}
269+
270+
configSecret.Data["talosconfig"] = []byte(talosConfig)
271+
272+
err = r.Client.Update(ctx, configSecret)
273+
if k8serrors.IsConflict(err) {
274+
// ignore conflict errors, probably another reconcile fixed up the endpoints
275+
err = nil
276+
}
277+
278+
return err
279+
}

controllers/talosconfig_controller.go

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ package controllers
66

77
import (
88
"context"
9-
"encoding/base64"
109
"encoding/json"
1110
"errors"
1211
"fmt"
1312
"strconv"
1413
"strings"
14+
"time"
1515

1616
jsonpatch "github.com/evanphx/json-patch"
1717
"github.com/go-logr/logr"
@@ -21,6 +21,7 @@ import (
2121
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate"
2222
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
2323
"github.com/talos-systems/talos/pkg/machinery/constants"
24+
"github.com/talos-systems/talos/pkg/machinery/role"
2425
"gopkg.in/yaml.v2"
2526
apierrors "k8s.io/apimachinery/pkg/api/errors"
2627
"k8s.io/apimachinery/pkg/runtime"
@@ -29,6 +30,7 @@ import (
2930
expv1 "sigs.k8s.io/cluster-api/exp/api/v1alpha4"
3031
"sigs.k8s.io/cluster-api/feature"
3132
"sigs.k8s.io/cluster-api/util"
33+
"sigs.k8s.io/cluster-api/util/annotations"
3234
"sigs.k8s.io/cluster-api/util/conditions"
3335
"sigs.k8s.io/cluster-api/util/patch"
3436
"sigs.k8s.io/cluster-api/util/predicates"
@@ -70,18 +72,6 @@ type TalosConfigBundle struct {
7072
TalosConfig string
7173
}
7274

73-
type talosConfig struct {
74-
Context string
75-
Contexts map[string]*talosConfigContext
76-
}
77-
78-
type talosConfigContext struct {
79-
Target string
80-
CA string
81-
Crt string
82-
Key string
83-
}
84-
8575
func (r *TalosConfigReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
8676
r.Scheme = mgr.GetScheme()
8777

@@ -148,6 +138,7 @@ func (r *TalosConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
148138
if err != nil {
149139
return ctrl.Result{}, err
150140
}
141+
151142
// Always attempt to Patch the TalosConfig object and status after each reconciliation.
152143
defer func() {
153144
// always update the readyCondition; the summary is represented using the "1 of x completed" notation.
@@ -156,14 +147,16 @@ func (r *TalosConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
156147
bootstrapv1alpha3.DataSecretAvailableCondition,
157148
),
158149
)
159-
// Patch ObservedGeneration only if the reconciliation completed successfully
150+
160151
patchOpts := []patch.Option{
161152
patch.WithOwnedConditions{
162153
Conditions: []capiv1.ConditionType{
163154
bootstrapv1alpha3.DataSecretAvailableCondition,
164155
},
165156
},
166157
}
158+
159+
// Patch ObservedGeneration only if the reconciliation completed successfully
167160
if rerr == nil {
168161
patchOpts = append(patchOpts, patch.WithStatusObservedGeneration{})
169162
}
@@ -214,11 +207,32 @@ func (r *TalosConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
214207
return ctrl.Result{}, err
215208
}
216209

210+
if annotations.IsPaused(cluster, config) {
211+
log.Info("Reconciliation is paused for this object")
212+
return ctrl.Result{}, nil
213+
}
214+
215+
tcScope := &TalosConfigScope{
216+
Config: config,
217+
ConfigOwner: owner,
218+
Cluster: cluster,
219+
}
220+
217221
// bail super early if it's already ready
218222
if config.Status.Ready {
219223
log.Info("ignoring an already ready config")
220224
conditions.MarkTrue(config, bootstrapv1alpha3.DataSecretAvailableCondition)
221-
return ctrl.Result{}, nil
225+
226+
// reconcile cluster-wide talosconfig
227+
err = r.reconcileClientConfig(ctx, log, tcScope)
228+
229+
if err == nil {
230+
conditions.MarkTrue(config, bootstrapv1alpha3.ClientConfigAvailableCondition)
231+
} else {
232+
conditions.MarkFalse(config, bootstrapv1alpha3.ClientConfigAvailableCondition, bootstrapv1alpha3.ClientConfigGenerationFailedReason, capiv1.ConditionSeverityError, "talosconfig generation failure: %s", err)
233+
}
234+
235+
return ctrl.Result{}, err
222236
}
223237

224238
// Wait patiently for the infrastructure to be ready
@@ -239,12 +253,6 @@ func (r *TalosConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
239253
return ctrl.Result{}, nil
240254
}
241255

242-
tcScope := &TalosConfigScope{
243-
Config: config,
244-
ConfigOwner: owner,
245-
Cluster: cluster,
246-
}
247-
248256
if err = r.reconcileGenerate(ctx, tcScope); err != nil {
249257
conditions.MarkFalse(config, bootstrapv1alpha3.DataSecretAvailableCondition, bootstrapv1alpha3.DataSecretGenerationFailedReason, capiv1.ConditionSeverityError, err.Error())
250258

@@ -341,7 +349,7 @@ func (r *TalosConfigReconciler) reconcileGenerate(ctx context.Context, tcScope *
341349
}
342350

343351
config.Status.DataSecretName = &dataSecretName
344-
config.Status.TalosConfig = retData.TalosConfig
352+
config.Status.TalosConfig = retData.TalosConfig //nolint:staticcheck // deprecated, for backwards compatibility only
345353

346354
return nil
347355
}
@@ -352,19 +360,31 @@ func (r *TalosConfigReconciler) reconcileDelete(ctx context.Context, config *boo
352360
return ctrl.Result{}, nil
353361
}
354362

355-
func genTalosConfigFile(clusterName string, certs *generate.Certs) (string, error) {
356-
talosConfig := &talosConfig{
357-
Context: clusterName,
358-
Contexts: map[string]*talosConfigContext{
359-
clusterName: {
360-
Target: "",
361-
CA: base64.StdEncoding.EncodeToString(certs.OS.Crt),
362-
Crt: base64.StdEncoding.EncodeToString(certs.Admin.Crt),
363-
Key: base64.StdEncoding.EncodeToString(certs.Admin.Key),
364-
},
363+
func genTalosConfigFile(clusterName string, bundle *generate.SecretsBundle, endpoints []string) (string, error) {
364+
in := &generate.Input{
365+
ClusterName: clusterName,
366+
Certs: &generate.Certs{
367+
OS: bundle.Certs.OS,
365368
},
366369
}
367370

371+
var err error
372+
373+
in.Certs.Admin, err = generate.NewAdminCertificateAndKey(
374+
bundle.Clock.Now(),
375+
bundle.Certs.OS,
376+
role.MakeSet(role.Admin),
377+
87600*time.Hour,
378+
)
379+
if err != nil {
380+
return "", err
381+
}
382+
383+
talosConfig, err := generate.Talosconfig(in, generate.WithEndpointList(endpoints))
384+
if err != nil {
385+
return "", err
386+
}
387+
368388
talosConfigBytes, err := yaml.Marshal(talosConfig)
369389
if err != nil {
370390
return "", err
@@ -398,6 +418,15 @@ func (r *TalosConfigReconciler) userConfigs(ctx context.Context, scope *TalosCon
398418

399419
retBundle.BootstrapData = userConfigStr
400420

421+
if userConfig.Machine().Security().CA() != nil && len(userConfig.Machine().Security().CA().Crt) > 0 && len(userConfig.Machine().Security().CA().Key) > 0 {
422+
bundle := generate.NewSecretsBundleFromConfig(generate.NewClock(), userConfig)
423+
424+
retBundle.TalosConfig, err = genTalosConfigFile(userConfig.Cluster().Name(), bundle, nil)
425+
if err != nil {
426+
r.Log.Error(err, "failed generating talosconfig for user-supplied machine configuration")
427+
}
428+
}
429+
401430
return retBundle, nil
402431
}
403432

@@ -453,7 +482,7 @@ func (r *TalosConfigReconciler) genConfigs(ctx context.Context, scope *TalosConf
453482

454483
genOptions = append(genOptions, generate.WithVersionContract(versionContract))
455484

456-
secretBundle, err := r.getSecretsBundle(ctx, scope, scope.Cluster.Name+"-talos", genOptions...)
485+
secretBundle, err := r.getSecretsBundle(ctx, scope, true, genOptions...)
457486
if err != nil {
458487
return retBundle, err
459488
}
@@ -476,7 +505,7 @@ func (r *TalosConfigReconciler) genConfigs(ctx context.Context, scope *TalosConf
476505
return retBundle, err
477506
}
478507

479-
tcString, err := genTalosConfigFile(input.ClusterName, input.Certs)
508+
tcString, err := genTalosConfigFile(input.ClusterName, secretBundle, nil)
480509
if err != nil {
481510
return retBundle, err
482511
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/talos-systems/talos/pkg/machinery v0.13.0
1313
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6
1414
gopkg.in/yaml.v2 v2.4.0
15+
inet.af/netaddr v0.0.0-20210903134321-85fa6c94624e
1516
k8s.io/api v0.21.4
1617
k8s.io/apiextensions-apiserver v0.21.4
1718
k8s.io/apimachinery v0.21.4
@@ -28,6 +29,8 @@ require (
2829
github.com/cespare/xxhash/v2 v2.1.1 // indirect
2930
github.com/containerd/go-cni v1.1.0 // indirect
3031
github.com/containernetworking/cni v1.0.1 // indirect
32+
github.com/coredns/caddy v1.1.0 // indirect
33+
github.com/coredns/corefile-migration v1.0.13 // indirect
3134
github.com/cosi-project/runtime v0.0.0-20210906201716-5cb7f5002d77 // indirect
3235
github.com/davecgh/go-spew v1.1.1 // indirect
3336
github.com/docker/distribution v2.7.1+incompatible // indirect
@@ -85,6 +88,8 @@ require (
8588
go.uber.org/atomic v1.7.0 // indirect
8689
go.uber.org/multierr v1.7.0 // indirect
8790
go.uber.org/zap v1.19.0 // indirect
91+
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 // indirect
92+
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
8893
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
8994
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
9095
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914 // indirect

0 commit comments

Comments
 (0)