From 84e712a9f1626b78638a4fe2524a2b860cb7185f Mon Sep 17 00:00:00 2001 From: Utku Ozdemir Date: Thu, 28 Jul 2022 19:45:04 +0200 Subject: [PATCH] feat: introduce Talos API access from Kubernetes We add a new CRD, `serviceaccounts.talos.dev` (with `tsa` as short name), and its controller which allows users to get a `Secret` containing a short-lived Talosconfig in their namespaces with the roles they need. Additionally, we introduce the `talosctl inject serviceaccount` command to accept a YAML file with Kubernetes manifests and inject them with Talos service accounts so that they can be directly applied to Kubernetes afterwards. If Talos API access feature is enabled on Talos side, the injected workloads will be able to talk to Talos API. Closes siderolabs/talos#4422. Signed-off-by: Utku Ozdemir --- cmd/talosctl/cmd/mgmt/cluster/create.go | 16 +- cmd/talosctl/cmd/mgmt/inject/inject.go | 14 + .../cmd/mgmt/inject/serviceaccount.go | 68 ++ cmd/talosctl/cmd/mgmt/root.go | 2 + cmd/talosctl/cmd/root.go | 19 +- hack/release.toml | 26 + .../machined/pkg/controllers/k8s/manifest.go | 23 + .../pkg/controllers/k8s/manifest_apply.go | 33 +- .../machined/pkg/controllers/k8s/templates.go | 40 ++ .../pkg/controllers/kubeaccess/endpoint.go | 68 +- .../controllers/kubeaccess/serviceaccount.go | 204 ++++++ .../serviceaccount/crd_controller.go | 644 ++++++++++++++++++ .../runtime/v1alpha2/v1alpha2_controller.go | 1 + internal/integration/api/serviceaccount.go | 303 ++++++++ internal/integration/base/k8s.go | 55 ++ internal/integration/cli/inject.go | 95 +++ .../testdata/inject/talosconfig-expected.yaml | 302 ++++++++ .../testdata/inject/talosconfig-input.yaml | 159 +++++ internal/integration/integration_test.go | 14 +- internal/integration/provision/upgrade.go | 7 +- internal/pkg/etcd/lock.go | 44 ++ internal/pkg/tui/installer/installer.go | 29 +- pkg/kubernetes/inject/serviceaccount.go | 378 ++++++++++ pkg/machinery/client/config/config.go | 92 ++- pkg/machinery/client/config/path.go | 89 ++- pkg/machinery/client/options.go | 7 +- .../types/v1alpha1/v1alpha1_validation.go | 1 - pkg/machinery/constants/constants.go | 32 +- .../advanced/talos-api-access-from-k8s.md | 157 +++++ website/content/v1.2/reference/cli.md | 200 ++++-- 30 files changed, 2872 insertions(+), 250 deletions(-) create mode 100644 cmd/talosctl/cmd/mgmt/inject/inject.go create mode 100644 cmd/talosctl/cmd/mgmt/inject/serviceaccount.go create mode 100644 internal/app/machined/pkg/controllers/kubeaccess/serviceaccount.go create mode 100644 internal/app/machined/pkg/controllers/kubeaccess/serviceaccount/crd_controller.go create mode 100644 internal/integration/api/serviceaccount.go create mode 100644 internal/integration/cli/inject.go create mode 100644 internal/integration/cli/testdata/inject/talosconfig-expected.yaml create mode 100644 internal/integration/cli/testdata/inject/talosconfig-input.yaml create mode 100644 internal/pkg/etcd/lock.go create mode 100644 pkg/kubernetes/inject/serviceaccount.go create mode 100644 website/content/v1.2/advanced/talos-api-access-from-k8s.md diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index 1d721bc5e..ac935071f 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -847,12 +847,16 @@ func trimVersion(version string) string { } func init() { - defaultTalosConfig, err := clientconfig.GetDefaultPath() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to find default Talos config path: %s", err) - } - - createCmd.Flags().StringVar(&talosconfig, "talosconfig", defaultTalosConfig, "The path to the Talos configuration file") + createCmd.Flags().StringVar( + &talosconfig, + "talosconfig", + "", + fmt.Sprintf("The path to the Talos configuration file. Defaults to '%s' env variable if set, otherwise '%s' and '%s' in order.", + constants.TalosConfigEnvVar, + filepath.Join("$HOME", constants.TalosDir, constants.TalosconfigFilename), + filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename), + ), + ) createCmd.Flags().StringVar(&nodeImage, "image", helpers.DefaultImage(images.DefaultTalosImageRepository), "the image to use") createCmd.Flags().StringVar(&nodeInstallImage, nodeInstallImageFlag, helpers.DefaultImage(images.DefaultInstallerImageRepository), "the installer image to use") createCmd.Flags().StringVar(&nodeVmlinuzPath, "vmlinuz-path", helpers.ArtifactPath(constants.KernelAssetWithArch), "the compressed kernel image to use") diff --git a/cmd/talosctl/cmd/mgmt/inject/inject.go b/cmd/talosctl/cmd/mgmt/inject/inject.go new file mode 100644 index 000000000..7ac8b099c --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/inject/inject.go @@ -0,0 +1,14 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package inject + +import "github.com/spf13/cobra" + +// Cmd represents the debug command. +var Cmd = &cobra.Command{ + Use: "inject", + Short: "Inject Talos API resources into Kubernetes manifests", + Long: ``, +} diff --git a/cmd/talosctl/cmd/mgmt/inject/serviceaccount.go b/cmd/talosctl/cmd/mgmt/inject/serviceaccount.go new file mode 100644 index 000000000..69d226e0f --- /dev/null +++ b/cmd/talosctl/cmd/mgmt/inject/serviceaccount.go @@ -0,0 +1,68 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package inject + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/talos-systems/talos/pkg/kubernetes/inject" + "github.com/talos-systems/talos/pkg/machinery/constants" +) + +var serviceAccountCmdFlags struct { + file string + roles []string +} + +var serviceAccountCmd = &cobra.Command{ + Use: fmt.Sprintf("%s [--roles=','] -f ", constants.ServiceAccountResourceSingular), + Aliases: []string{constants.ServiceAccountResourceShortName}, + Short: "Inject Talos API ServiceAccount into Kubernetes manifests", + Example: fmt.Sprintf( + `talosctl inject %[1]s --roles="os:admin" -f deployment.yaml > deployment-injected.yaml + +Alternatively, stdin can be piped to the command: +cat deployment.yaml | talosctl inject %[1]s --roles="os:admin" -f - > deployment-injected.yaml +`, + constants.ServiceAccountResourceSingular, + ), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + var err error + + if serviceAccountCmdFlags.file == "" { + return cmd.Help() + } + + reader := os.Stdin + + if serviceAccountCmdFlags.file != "-" { + reader, err = os.Open(serviceAccountCmdFlags.file) + if err != nil { + return err + } + } + + injectedYaml, err := inject.ServiceAccount(reader, serviceAccountCmdFlags.roles) + if err != nil { + return err + } + + fmt.Println(string(injectedYaml)) + + return nil + }, +} + +func init() { + serviceAccountCmd.Flags().StringVarP(&serviceAccountCmdFlags.file, "file", "f", "", + fmt.Sprintf("file with Kubernetes manifests to be injected with %s", constants.ServiceAccountResourceKind)) + serviceAccountCmd.Flags().StringSliceVarP(&serviceAccountCmdFlags.roles, "roles", "r", []string{"os:reader"}, + fmt.Sprintf("roles to add to the generated %s manifests", constants.ServiceAccountResourceKind)) + Cmd.AddCommand(serviceAccountCmd) +} diff --git a/cmd/talosctl/cmd/mgmt/root.go b/cmd/talosctl/cmd/mgmt/root.go index d480d4714..608f5f90c 100644 --- a/cmd/talosctl/cmd/mgmt/root.go +++ b/cmd/talosctl/cmd/mgmt/root.go @@ -10,6 +10,7 @@ import ( "github.com/talos-systems/talos/cmd/talosctl/cmd/mgmt/cluster" "github.com/talos-systems/talos/cmd/talosctl/cmd/mgmt/debug" "github.com/talos-systems/talos/cmd/talosctl/cmd/mgmt/gen" + "github.com/talos-systems/talos/cmd/talosctl/cmd/mgmt/inject" ) // Commands is a list of commands published by the package. @@ -28,4 +29,5 @@ func init() { addCommand(cluster.Cmd) addCommand(gen.Cmd) addCommand(debug.Cmd) + addCommand(inject.Cmd) } diff --git a/cmd/talosctl/cmd/root.go b/cmd/talosctl/cmd/root.go index 1800373da..e49969422 100644 --- a/cmd/talosctl/cmd/root.go +++ b/cmd/talosctl/cmd/root.go @@ -7,6 +7,7 @@ package cmd import ( "fmt" "os" + "path/filepath" "strings" "github.com/spf13/cobra" @@ -14,7 +15,7 @@ import ( "github.com/talos-systems/talos/cmd/talosctl/cmd/mgmt" "github.com/talos-systems/talos/cmd/talosctl/cmd/talos" "github.com/talos-systems/talos/pkg/cli" - clientconfig "github.com/talos-systems/talos/pkg/machinery/client/config" + "github.com/talos-systems/talos/pkg/machinery/constants" ) // rootCmd represents the base command when called without any subcommands. @@ -30,12 +31,16 @@ var rootCmd = &cobra.Command{ // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() error { - defaultTalosConfig, err := clientconfig.GetDefaultPath() - if err != nil { - return err - } - - rootCmd.PersistentFlags().StringVar(&talos.Talosconfig, "talosconfig", defaultTalosConfig, "The path to the Talos configuration file") + rootCmd.PersistentFlags().StringVar( + &talos.Talosconfig, + "talosconfig", + "", + fmt.Sprintf("The path to the Talos configuration file. Defaults to '%s' env variable if set, otherwise '%s' and '%s' in order.", + constants.TalosConfigEnvVar, + filepath.Join("$HOME", constants.TalosDir, constants.TalosconfigFilename), + filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename), + ), + ) rootCmd.PersistentFlags().StringVar(&talos.Cmdcontext, "context", "", "Context to be used in command") rootCmd.PersistentFlags().StringSliceVarP(&talos.Nodes, "nodes", "n", []string{}, "target the specified nodes") rootCmd.PersistentFlags().StringSliceVarP(&talos.Endpoints, "endpoints", "e", []string{}, "override default endpoints in Talos configuration") diff --git a/hack/release.toml b/hack/release.toml index 504321171..521b3e16e 100644 --- a/hack/release.toml +++ b/hack/release.toml @@ -14,6 +14,32 @@ preface = """\ """ [notes] + [notes.api-access-from-kubernetes] + title = "Talos API access from Kubernetes" + description = """\ +Talos now supports access to its API from within Kubernetes. It can be configured in the machine config as below: +```yaml +machine: + features: + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:reader + allowedKubernetesNamespaces: + - kube-system +``` + +This feature introduces a new custom resource definition, `serviceaccounts.talos.dev`. +Creating custom resources of this type will provide credentials to access Talos API from within Kubernetes. + +The new CLI subcommand `talosctl inject serviceaccount` can be used to configure Kubernetes manifests with Talos service accounts as below: +``` +talosctl inject serviceaccount -f manifests.yaml > manifests-injected.yaml +kubectl apply -f manifests-injected.yaml +``` + +See [documentation](https://www.talos.dev/v1.2/advanced/configuration/talos-api-access-from-k8s/) for more details. +""" [notes.seccomp] title = "Seccomp Profiles" diff --git a/internal/app/machined/pkg/controllers/k8s/manifest.go b/internal/app/machined/pkg/controllers/k8s/manifest.go index c5e28b65a..b624d1d21 100644 --- a/internal/app/machined/pkg/controllers/k8s/manifest.go +++ b/internal/app/machined/pkg/controllers/k8s/manifest.go @@ -165,6 +165,8 @@ func (ctrl *ManifestController) render(cfg k8s.BootstrapManifestsConfigSpec, scr KubernetesTalosAPIServiceNamespace string ApidPort int + + TalosServiceAccount TalosServiceAccount }{ BootstrapManifestsConfigSpec: cfg, Secrets: scrt, @@ -173,6 +175,15 @@ func (ctrl *ManifestController) render(cfg k8s.BootstrapManifestsConfigSpec, scr KubernetesTalosAPIServiceNamespace: constants.KubernetesTalosAPIServiceNamespace, ApidPort: constants.ApidPort, + + TalosServiceAccount: TalosServiceAccount{ + Group: constants.ServiceAccountResourceGroup, + Version: constants.ServiceAccountResourceVersion, + Kind: constants.ServiceAccountResourceKind, + ResourceSingular: constants.ServiceAccountResourceSingular, + ResourcePlural: constants.ServiceAccountResourcePlural, + ShortName: constants.ServiceAccountResourceShortName, + }, } type manifestDesc struct { @@ -226,6 +237,7 @@ func (ctrl *ManifestController) render(cfg k8s.BootstrapManifestsConfigSpec, scr defaultManifests = append(defaultManifests, []manifestDesc{ {"12-talos-api-service", talosAPIService}, + {"13-talos-service-account-crd", talosServiceAccountCRDTemplate}, }..., ) } @@ -276,3 +288,14 @@ func (ctrl *ManifestController) teardownAll(ctx context.Context, r controller.Ru return nil } + +// TalosServiceAccount is a struct used by the template engine which contains the needed variables to +// be able to construct the Talos Service Account CRD. +type TalosServiceAccount struct { + Group string + Version string + Kind string + ResourceSingular string + ResourcePlural string + ShortName string +} diff --git a/internal/app/machined/pkg/controllers/k8s/manifest_apply.go b/internal/app/machined/pkg/controllers/k8s/manifest_apply.go index 90292af86..6219c4c1c 100644 --- a/internal/app/machined/pkg/controllers/k8s/manifest_apply.go +++ b/internal/app/machined/pkg/controllers/k8s/manifest_apply.go @@ -13,7 +13,6 @@ import ( "github.com/cosi-project/runtime/pkg/resource" "github.com/cosi-project/runtime/pkg/state" "github.com/siderolabs/go-pointer" - "go.etcd.io/etcd/client/v3/concurrency" "go.uber.org/zap" "go.uber.org/zap/zapcore" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -155,7 +154,7 @@ func (ctrl *ManifestApplyController) Run(ctx context.Context, r controller.Runti return fmt.Errorf("error building dynamic client: %w", err) } - if err = ctrl.etcdLock(ctx, logger, func() error { + if err = etcd.WithLock(ctx, constants.EtcdTalosManifestApplyMutex, logger, func() error { return ctrl.apply(ctx, logger, mapper, dyn, manifests) }); err != nil { return err @@ -176,36 +175,6 @@ func (ctrl *ManifestApplyController) Run(ctx context.Context, r controller.Runti } } -func (ctrl *ManifestApplyController) etcdLock(ctx context.Context, logger *zap.Logger, f func() error) error { - etcdClient, err := etcd.NewLocalClient() - if err != nil { - return fmt.Errorf("error creating etcd client: %w", err) - } - - defer etcdClient.Close() //nolint:errcheck - - session, err := concurrency.NewSession(etcdClient.Client) - if err != nil { - return fmt.Errorf("error creating etcd session: %w", err) - } - - defer session.Close() //nolint:errcheck - - mutex := concurrency.NewMutex(session, constants.EtcdTalosManifestApplyMutex) - - logger.Debug("waiting for mutex") - - if err := mutex.Lock(ctx); err != nil { - return fmt.Errorf("error acquiring mutex: %w", err) - } - - logger.Debug("mutex acquired") - - defer mutex.Unlock(ctx) //nolint:errcheck - - return f() -} - //nolint:gocyclo func (ctrl *ManifestApplyController) apply(ctx context.Context, logger *zap.Logger, mapper *restmapper.DeferredDiscoveryRESTMapper, dyn dynamic.Interface, manifests resource.List) error { // flatten list of objects to be applied diff --git a/internal/app/machined/pkg/controllers/k8s/templates.go b/internal/app/machined/pkg/controllers/k8s/templates.go index 73f612486..ff9ab4011 100644 --- a/internal/app/machined/pkg/controllers/k8s/templates.go +++ b/internal/app/machined/pkg/controllers/k8s/templates.go @@ -718,3 +718,43 @@ spec: protocol: TCP targetPort: {{ .ApidPort }} `) + +// talosServiceAccountCRDTemplate is the template of the CRD which +// allows injecting Talos with credentials into the Kubernetes cluster. +var talosServiceAccountCRDTemplate = []byte(`apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: {{ .TalosServiceAccount.ResourcePlural }}.{{ .TalosServiceAccount.Group }} +spec: + conversion: + strategy: None + group: {{ .TalosServiceAccount.Group }} + names: + kind: {{ .TalosServiceAccount.Kind }} + listKind: {{ .TalosServiceAccount.Kind }}List + plural: {{ .TalosServiceAccount.ResourcePlural }} + singular: {{ .TalosServiceAccount.ResourceSingular }} + shortNames: + - {{ .TalosServiceAccount.ShortName }} + scope: Namespaced + versions: + - name: {{ .TalosServiceAccount.Version }} + schema: + openAPIV3Schema: + properties: + spec: + type: object + properties: + roles: + type: array + items: + type: string + status: + type: object + properties: + failureReason: + type: string + type: object + served: true + storage: true +`) diff --git a/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go b/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go index e5c91e575..85425680b 100644 --- a/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go +++ b/internal/app/machined/pkg/controllers/kubeaccess/endpoint.go @@ -39,57 +39,7 @@ func (ctrl *EndpointController) Name() string { // Inputs implements controller.Controller interface. func (ctrl *EndpointController) Inputs() []controller.Input { - return nil -} - -// Outputs implements controller.Controller interface. -func (ctrl *EndpointController) Outputs() []controller.Output { - return nil -} - -// Run implements controller.Controller interface. -// -//nolint:gocyclo -func (ctrl *EndpointController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { - for { - if err := r.UpdateInputs([]controller.Input{ - { - Namespace: config.NamespaceName, - Type: kubeaccess.ConfigType, - ID: pointer.To(kubeaccess.ConfigID), - Kind: controller.InputWeak, - }, - }); err != nil { - return err - } - - select { - case <-ctx.Done(): - return nil - case <-r.EventCh(): - } - - kubeaccessConfig, err := r.Get(ctx, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID).Metadata()) - if err != nil { - if !state.IsNotFoundError(err) { - return fmt.Errorf("error fetching kubeaccess config: %w", err) - } - } - - if kubeaccessConfig == nil || !kubeaccessConfig.(*kubeaccess.Config).TypedSpec().Enabled { - // disabled, nothing to do - continue - } - - if err = ctrl.reconcile(ctx, r, logger); err != nil { - return err - } - } -} - -//nolint:gocyclo -func (ctrl *EndpointController) reconcile(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { - if err := r.UpdateInputs([]controller.Input{ + return []controller.Input{ { Namespace: config.NamespaceName, Type: kubeaccess.ConfigType, @@ -107,12 +57,18 @@ func (ctrl *EndpointController) reconcile(ctx context.Context, r controller.Runt Type: k8s.EndpointType, Kind: controller.InputWeak, }, - }); err != nil { - return err } +} - r.QueueReconcile() +// Outputs implements controller.Controller interface. +func (ctrl *EndpointController) Outputs() []controller.Output { + return nil +} +// Run implements controller.Controller interface. +// +//nolint:gocyclo +func (ctrl *EndpointController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { for { select { case <-r.EventCh(): @@ -128,8 +84,8 @@ func (ctrl *EndpointController) reconcile(ctx context.Context, r controller.Runt } if kubeaccessConfig == nil || !kubeaccessConfig.(*kubeaccess.Config).TypedSpec().Enabled { - // disabled, bail out - return nil + // disabled, do not do anything + continue } endpointResources, err := r.List(ctx, resource.NewMetadata(k8s.ControlPlaneNamespaceName, k8s.EndpointType, "", resource.VersionUndefined)) diff --git a/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount.go b/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount.go new file mode 100644 index 000000000..9d2351470 --- /dev/null +++ b/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount.go @@ -0,0 +1,204 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package kubeaccess + +import ( + "context" + "errors" + "fmt" + + "github.com/cosi-project/runtime/pkg/controller" + "github.com/cosi-project/runtime/pkg/resource" + "github.com/cosi-project/runtime/pkg/safe" + "github.com/cosi-project/runtime/pkg/state" + "github.com/siderolabs/go-pointer" + "github.com/talos-systems/crypto/x509" + "go.uber.org/zap" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount" + "github.com/talos-systems/talos/internal/pkg/etcd" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/resources/config" + "github.com/talos-systems/talos/pkg/machinery/resources/kubeaccess" + "github.com/talos-systems/talos/pkg/machinery/resources/secrets" +) + +// CRDController manages Kubernetes endpoints resource for Talos API endpoints. +type CRDController struct{} + +// Name implements controller.Controller interface. +func (ctrl *CRDController) Name() string { + return "kubeaccess.CRDController" +} + +// Inputs implements controller.Controller interface. +func (ctrl *CRDController) Inputs() []controller.Input { + return []controller.Input{ + { + Namespace: config.NamespaceName, + Type: kubeaccess.ConfigType, + ID: pointer.To(kubeaccess.ConfigID), + Kind: controller.InputWeak, + }, + { + Namespace: secrets.NamespaceName, + Type: secrets.KubernetesType, + ID: pointer.To(secrets.KubernetesID), + Kind: controller.InputWeak, + }, + { + Namespace: secrets.NamespaceName, + Type: secrets.OSRootType, + ID: pointer.To(secrets.OSRootID), + Kind: controller.InputWeak, + }, + } +} + +// Outputs implements controller.Controller interface. +func (ctrl *CRDController) Outputs() []controller.Output { + return nil +} + +// Run implements controller.Controller interface. +// +//nolint:gocyclo,cyclop +func (ctrl *CRDController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error { + var crdControllerCtxCancel context.CancelFunc + + crdControllerErrCh := make(chan error, 1) + + stopCRDController := func() { + if crdControllerCtxCancel != nil { + crdControllerCtxCancel() + + <-crdControllerErrCh + + crdControllerCtxCancel = nil + } + } + + defer stopCRDController() + + for { + select { + case <-ctx.Done(): + return nil //nolint:govet + case <-r.EventCh(): + case err := <-crdControllerErrCh: + if crdControllerCtxCancel != nil { + crdControllerCtxCancel() + } + + crdControllerCtxCancel = nil + + if err != nil && !errors.Is(err, context.Canceled) { + return fmt.Errorf("error from crd controller: %w", err) + } + } + + kubeaccessConfig, err := safe.ReaderGet[*kubeaccess.Config](ctx, r, kubeaccess.NewConfig(config.NamespaceName, kubeaccess.ConfigID).Metadata()) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error fetching kubeaccess config: %w", err) + } + + continue + } + + var kubeaccessConfigSpec *kubeaccess.ConfigSpec + + if kubeaccessConfig != nil { + kubeaccessConfigSpec = kubeaccessConfig.TypedSpec() + } + + if kubeaccessConfig == nil || kubeaccessConfigSpec == nil || !kubeaccessConfigSpec.Enabled { + stopCRDController() + + continue + } + + kubeSecretsResources, err := safe.ReaderGet[*secrets.Kubernetes](ctx, r, resource.NewMetadata( + secrets.NamespaceName, + secrets.KubernetesType, + secrets.KubernetesID, + resource.VersionUndefined, + )) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error fetching kubernetes secrets: %w", err) + } + + continue + } + + kubeSecretsSpec := kubeSecretsResources.TypedSpec() + + osSecretsResource, err := safe.ReaderGet[*secrets.OSRoot](ctx, r, resource.NewMetadata( + secrets.NamespaceName, + secrets.OSRootType, + secrets.OSRootID, + resource.VersionUndefined, + )) + if err != nil { + if !state.IsNotFoundError(err) { + return fmt.Errorf("error fetching os secrets: %w", err) + } + + continue + } + + osSecretsSpec := osSecretsResource.TypedSpec() + + kubeconfig, err := clientcmd.BuildConfigFromKubeconfigGetter("", func() (*clientcmdapi.Config, error) { + return clientcmd.Load([]byte(kubeSecretsSpec.LocalhostAdminKubeconfig)) + }) + if err != nil { + return fmt.Errorf("error loading kubeconfig: %w", err) + } + + stopCRDController() + + var crdControllerCtx context.Context + + crdControllerCtx, crdControllerCtxCancel = context.WithCancel(ctx) //nolint:govet + + go func() { + crdControllerErrCh <- ctrl.runCRDController( + crdControllerCtx, + osSecretsSpec.CA, + kubeconfig, + kubeaccessConfigSpec, + logger, + ) + }() + } +} + +func (ctrl *CRDController) runCRDController( + ctx context.Context, + talosCA *x509.PEMEncodedCertificateAndKey, + kubeconfig *rest.Config, + kubeaccessCfgSpec *kubeaccess.ConfigSpec, + logger *zap.Logger, +) error { + return etcd.WithLock(ctx, constants.EtcdTalosServiceAccountCRDControllerMutex, logger, func() error { + crdCtrl, err := serviceaccount.NewCRDController( + talosCA, + kubeconfig, + kubeaccessCfgSpec.AllowedKubernetesNamespaces, + kubeaccessCfgSpec.AllowedAPIRoles, + logger, + ) + if err != nil { + return err + } + + return crdCtrl.Run(ctx, 1) + }) +} diff --git a/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount/crd_controller.go b/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount/crd_controller.go new file mode 100644 index 000000000..011554ebb --- /dev/null +++ b/internal/app/machined/pkg/controllers/kubeaccess/serviceaccount/crd_controller.go @@ -0,0 +1,644 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package serviceaccount + +import ( + "bytes" + "context" + stdlibx509 "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "reflect" + "sort" + "sync" + "time" + + "github.com/talos-systems/crypto/x509" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + kubeerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/dynamic/dynamiclister" + kubeinformers "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/connrotation" + "k8s.io/client-go/util/workqueue" + + taloskubernetes "github.com/talos-systems/talos/pkg/kubernetes" + clientconfig "github.com/talos-systems/talos/pkg/machinery/client/config" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/generate" + "github.com/talos-systems/talos/pkg/machinery/constants" + "github.com/talos-systems/talos/pkg/machinery/generic/slices" + "github.com/talos-systems/talos/pkg/machinery/role" +) + +const ( + certTTL = time.Hour * 6 + certRenewThreshold = time.Hour * 1 + + successResourceSynced = "Synced" + messageResourceSynced = "Synced successfully" + + errResourceExists = "ErrResourceExists" + messageResourceExists = "%s already exists and is not managed by controller: %s" + + errRolesNotFound = "ErrRolesNotFound" + messageRolesNotFound = "Roles not found" + + errNamespaceNotAllowed = "ErrNamespaceNotAllowed" + messageNamespaceNotAllowed = "Namespace is not allowed: %s" + + errRolesNotAllowed = "ErrRolesNotAllowed" + messageRolesNotAllowed = "Roles not allowed: %v" + + controllerAgentName = "talos-sa-controller" + informerResyncPeriod = time.Minute * 1 + + talosconfigContextName = "default" + endpoint = constants.KubernetesTalosAPIServiceName + "." + constants.KubernetesTalosAPIServiceNamespace + + kindSecret = "Secret" +) + +var ( + talosSAGV = schema.GroupVersion{ + Group: constants.ServiceAccountResourceGroup, + Version: constants.ServiceAccountResourceVersion, + } + + talosSAGVR = talosSAGV.WithResource(constants.ServiceAccountResourcePlural) + talosSAGVK = talosSAGV.WithKind(constants.ServiceAccountResourceKind) +) + +// CRDController is the controller implementation for TalosServiceAccount resources. +type CRDController struct { + talosCA *x509.PEMEncodedCertificateAndKey + + allowedNamespaces []string + allowedRoles map[string]struct{} + + queue workqueue.RateLimitingInterface + + kubeInformerFactory kubeinformers.SharedInformerFactory + dynamicInformerFactory dynamicinformer.DynamicSharedInformerFactory + + kubeClient kubernetes.Interface + dynamicClient dynamic.Interface + dialer *connrotation.Dialer + + secretsSynced cache.InformerSynced + talosSAsSynced cache.InformerSynced + + secretsLister corelisters.SecretLister + dynamicLister dynamiclister.Lister + + eventRecorder record.EventRecorder + + logger *zap.Logger +} + +// NewCRDController creates a new CRD controller. +func NewCRDController( + talosCA *x509.PEMEncodedCertificateAndKey, + kubeconfig *rest.Config, + allowedNamespaces []string, + allowedRoles []string, + logger *zap.Logger, +) (*CRDController, error) { + dialer := taloskubernetes.NewDialer() + kubeconfig.Dial = dialer.DialContext + + kubeCli, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return nil, err + } + + dynCli, err := dynamic.NewForConfig(kubeconfig) + if err != nil { + return nil, err + } + + dynamicInformerFactory := dynamicinformer.NewDynamicSharedInformerFactory(dynCli, informerResyncPeriod) + resourceInformer := dynamicInformerFactory.ForResource(talosSAGVR) + informer := resourceInformer.Informer() + + indexer := informer.GetIndexer() + lister := dynamiclister.New(indexer, talosSAGVR) + + kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeCli, informerResyncPeriod) + secrets := kubeInformerFactory.Core().V1().Secrets() + + logger.Debug("creating event broadcaster") + + eventBroadcaster := record.NewBroadcaster() + eventBroadcaster.StartStructuredLogging(0) + eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeCli.CoreV1().Events("")}) + + recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName}) + + controller := CRDController{ + talosCA: talosCA, + allowedNamespaces: allowedNamespaces, + allowedRoles: slices.ToSet(allowedRoles), + dynamicInformerFactory: dynamicInformerFactory, + kubeInformerFactory: kubeInformerFactory, + kubeClient: kubeCli, + dynamicClient: dynCli, + dialer: dialer, + dynamicLister: lister, + queue: workqueue.NewNamedRateLimitingQueue( + workqueue.DefaultControllerRateLimiter(), + constants.ServiceAccountResourceKind, + ), + logger: logger, + secretsSynced: secrets.Informer().HasSynced, + talosSAsSynced: informer.HasSynced, + eventRecorder: recorder, + secretsLister: secrets.Lister(), + } + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.enqueueTalosSA, + UpdateFunc: func(oldTalosSA, newTalosSA interface{}) { + controller.enqueueTalosSA(newTalosSA) + }, + }) + + secrets.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: controller.handleSecret, + UpdateFunc: func(oldSec, newSec interface{}) { + newSecret := newSec.(*corev1.Secret) //nolint:errcheck + oldSecret := oldSec.(*corev1.Secret) //nolint:errcheck + + if newSecret.ResourceVersion == oldSecret.ResourceVersion { + return + } + + controller.handleSecret(newSec) + }, + DeleteFunc: controller.handleSecret, + }) + + return &controller, nil +} + +// Run starts the CRD controller. +func (t *CRDController) Run(ctx context.Context, workers int) error { + var wg sync.WaitGroup + + defer func() { + t.queue.ShutDown() + t.dialer.CloseAll() + + wg.Wait() + t.logger.Debug("all workers have shut down") + }() + + t.kubeInformerFactory.Start(ctx.Done()) + t.dynamicInformerFactory.Start(ctx.Done()) + + t.logger.Sugar().Debugf("starting %s controller", constants.ServiceAccountResourceKind) + + t.logger.Debug("waiting for informer caches to sync") + + if ok := cache.WaitForCacheSync(ctx.Done(), t.secretsSynced, t.talosSAsSynced); !ok { + return fmt.Errorf("failed to wait for caches to sync") + } + + t.logger.Debug("starting workers") + + wg.Add(workers) + + for i := 0; i < workers; i++ { + go func() { + wait.Until(func() { t.runWorker(ctx) }, time.Second, ctx.Done()) + wg.Done() + }() + } + + t.logger.Debug("started workers") + + <-ctx.Done() + + t.logger.Debug("shutting down workers") + + return nil +} + +func (t *CRDController) runWorker(ctx context.Context) { + for t.processNextWorkItem(ctx) { + } +} + +func (t *CRDController) processNextWorkItem(ctx context.Context) bool { + obj, shutdown := t.queue.Get() + + if shutdown { + return false + } + + err := func(obj interface{}) error { + defer t.queue.Done(obj) + + var key string + + var ok bool + + if key, ok = obj.(string); !ok { + t.queue.Forget(obj) + utilruntime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj)) + + return nil + } + + if err := t.syncHandler(ctx, key); err != nil { + t.queue.AddRateLimited(key) + + return fmt.Errorf("error syncing '%s': %s, requeuing", key, err.Error()) + } + + t.queue.Forget(obj) + t.logger.Sugar().Debugf("successfully synced '%s'", key) + + return nil + }(obj) + if err != nil { + utilruntime.HandleError(err) + + return true + } + + return true +} + +//nolint:gocyclo,cyclop,dupl +func (t *CRDController) syncHandler(ctx context.Context, key string) error { + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key)) + + return nil + } + + talosSA, err := t.dynamicLister.Namespace(namespace).Get(name) + if err != nil { + if kubeerrors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("talosSA '%s' in work queue no longer exists", key)) + + return nil + } + + return err + } + + secret, err := t.secretsLister.Secrets(namespace).Get(name) + secretNotFound := kubeerrors.IsNotFound(err) + + if err != nil && !secretNotFound { + return err + } + + if !secretNotFound && !metav1.IsControlledBy(secret, talosSA) { + msg := fmt.Sprintf(messageResourceExists, kindSecret, key) + + err = t.updateTalosSAStatus(ctx, talosSA, msg) + if err != nil { + return err + } + + t.eventRecorder.Event(talosSA, corev1.EventTypeWarning, errResourceExists, msg) + + return errors.New(msg) + } + + desiredRoles, found, err := unstructured.NestedStringSlice(talosSA.UnstructuredContent(), "spec", "roles") + if err != nil || !found { + msg := fmt.Sprint(messageRolesNotFound) + + updateErr := t.updateTalosSAStatus(ctx, talosSA, msg) + if updateErr != nil { + return updateErr + } + + t.eventRecorder.Event(talosSA, corev1.EventTypeWarning, errRolesNotFound, messageRolesNotFound) + + if err != nil { + return fmt.Errorf("%s: %w", msg, err) + } + + return errors.New(msg) + } + + desiredRoleSet, _ := role.Parse(desiredRoles) + + if !slices.Contains(t.allowedNamespaces, func(allowedNS string) bool { + return allowedNS == namespace + }) { + msg := fmt.Sprintf(messageNamespaceNotAllowed, namespace) + + err = t.updateTalosSAStatus(ctx, talosSA, msg) + if err != nil { + return err + } + + t.eventRecorder.Event(talosSA, corev1.EventTypeWarning, errNamespaceNotAllowed, msg) + + return nil + } + + var unallowedRoles []string + + for _, desiredRole := range desiredRoles { + _, allowed := t.allowedRoles[desiredRole] + if !allowed { + unallowedRoles = append(unallowedRoles, desiredRole) + } + } + + if len(unallowedRoles) > 0 { + msg := fmt.Sprintf(messageRolesNotAllowed, unallowedRoles) + + err = t.updateTalosSAStatus(ctx, talosSA, msg) + if err != nil { + return err + } + + t.eventRecorder.Event(talosSA, corev1.EventTypeWarning, errRolesNotAllowed, msg) + + return nil + } + + if secretNotFound { + var newSecret *corev1.Secret + + newSecret, err = t.newSecret(talosSA, desiredRoleSet) + if err != nil { + return err + } + + _, err = t.kubeClient.CoreV1().Secrets(namespace).Create(ctx, newSecret, metav1.CreateOptions{}) + if err != nil { + return err + } + } else if t.needsUpdate(secret, desiredRoleSet.Strings()) { + var newTalosconfigBytes []byte + + newTalosconfigBytes, err = t.generateTalosconfig(desiredRoleSet) + if err != nil { + return err + } + + secret.Data[constants.TalosconfigFilename] = newTalosconfigBytes + + _, err = t.kubeClient.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{}) + if err != nil { + return err + } + } + + err = t.updateTalosSAStatus(ctx, talosSA, "") + if err != nil { + return err + } + + t.eventRecorder.Event(talosSA, corev1.EventTypeNormal, successResourceSynced, messageResourceSynced) + + return nil +} + +func (t *CRDController) enqueueTalosSA(obj interface{}) { + key, err := cache.MetaNamespaceKeyFunc(obj) + if err != nil { + utilruntime.HandleError(err) + + return + } + + t.queue.Add(key) +} + +func (t *CRDController) handleSecret(obj interface{}) { + var object metav1.Object + + var ok bool + + if object, ok = obj.(metav1.Object); !ok { + tombstone, tombstoneOK := obj.(cache.DeletedFinalStateUnknown) + if !tombstoneOK { + utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type")) + + return + } + + object, tombstoneOK = tombstone.Obj.(metav1.Object) + if !tombstoneOK { + utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type")) + + return + } + + t.logger.Sugar().Debugf("recovered deleted object '%s' from tombstone", object.GetName()) + } + + t.logger.Sugar().Debugf("processing object: %s", object.GetName()) + + if ownerRef := metav1.GetControllerOf(object); ownerRef != nil { + if ownerRef.Kind != constants.ServiceAccountResourceKind { + return + } + + talosSA, err := t.dynamicLister.Namespace(object.GetNamespace()).Get(ownerRef.Name) + if err != nil { + t.logger.Sugar().Debugf("ignoring orphaned object '%s/%s' of %s '%s'", + object.GetNamespace(), object.GetName(), constants.ServiceAccountResourceKind, ownerRef.Name) + + return + } + + t.enqueueTalosSA(talosSA) + + return + } +} + +func (t *CRDController) updateTalosSAStatus( + ctx context.Context, + talosSA *unstructured.Unstructured, + failureReason string, +) error { + var err error + + talosSACopy := talosSA.DeepCopy() + + if err != nil { + return err + } + + if failureReason == "" { + unstructured.RemoveNestedField(talosSACopy.UnstructuredContent(), "status", "failureReason") + + if err != nil { + return err + } + } else { + err = unstructured.SetNestedField(talosSACopy.UnstructuredContent(), failureReason, "status", "failureReason") + if err != nil { + return err + } + } + + _, err = t.dynamicClient.Resource(talosSAGVR). + Namespace(talosSACopy.GetNamespace()). + Update(ctx, talosSACopy, metav1.UpdateOptions{}) + + return err +} + +//nolint:gocyclo +func (t *CRDController) needsUpdate(secret *corev1.Secret, desiredRoles []string) bool { + talosconfigInSecret, ok := secret.Data[constants.TalosconfigFilename] + if !ok { + t.logger.Debug("talosconfig not found in secret", zap.String("key", constants.TalosconfigFilename)) + + return true + } + + parsedTalosconfigInSecret, err := clientconfig.ReadFrom(bytes.NewReader(talosconfigInSecret)) + if err != nil { + t.logger.Debug("error parsing talosconfig in secret", zap.Error(err)) + + return true + } + + talosconfigCtx := parsedTalosconfigInSecret.Contexts[parsedTalosconfigInSecret.Context] + + talosconfigCA, err := base64.StdEncoding.DecodeString(talosconfigCtx.CA) + if err != nil { + t.logger.Debug("error decoding talosconfig CA", zap.Error(err)) + + return true + } + + if !reflect.DeepEqual(t.talosCA.Crt, talosconfigCA) { + t.logger.Debug("ca mismatch detected") + + return true + } + + if len(talosconfigCtx.Endpoints) != 1 || talosconfigCtx.Endpoints[0] != endpoint { + t.logger.Debug( + "endpoint mismatch detected", + zap.Strings("actual", talosconfigCtx.Endpoints), + zap.Strings("expected", []string{endpoint}), + ) + + return true + } + + talosconfigCRT, err := base64.StdEncoding.DecodeString(talosconfigCtx.Crt) + if err != nil { + t.logger.Debug("error decoding talosconfig CRT", zap.Error(err)) + + return true + } + + block, _ := pem.Decode(talosconfigCRT) + if block == nil { + t.logger.Debug("could not decode talosconfig CRT") + + return true + } + + certificate, err := stdlibx509.ParseCertificate(block.Bytes) + if err != nil { + t.logger.Debug("error parsing certificate in talosconfig of secret", zap.Error(err)) + + return true + } + + if certificate.NotAfter.IsZero() { + t.logger.Debug("certificate in talosconfig of secret has no expiration date", zap.Error(err)) + + return true + } + + if time.Now().Add(certTTL).Before(certificate.NotAfter) { + t.logger.Debug( + "certificate in talosconfig has expiration date too far in the future", + zap.Time("expiration", certificate.NotAfter), + ) + + return true + } + + if time.Now().Add(certRenewThreshold).After(certificate.NotAfter) { + t.logger.Debug( + "certificate in talosconfig needs renewal", + zap.Time("expiration", certificate.NotAfter), + ) + + return true + } + + actualRoles := certificate.Subject.Organization + + sort.Strings(actualRoles) + sort.Strings(desiredRoles) + + if !reflect.DeepEqual(actualRoles, desiredRoles) { + t.logger.Debug("roles in certificate do not match desired roles", + zap.Strings("actual", actualRoles), zap.Strings("desired", desiredRoles)) + + return true + } + + return false +} + +func (t *CRDController) newSecret(talosSA *unstructured.Unstructured, roles role.Set) (*corev1.Secret, error) { + config, err := t.generateTalosconfig(roles) + if err != nil { + return nil, err + } + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: talosSA.GetName(), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(talosSA, talosSAGVK), + }, + }, + Data: map[string][]byte{ + constants.TalosconfigFilename: config, + }, + }, nil +} + +func (t *CRDController) generateTalosconfig(roles role.Set) ([]byte, error) { + var newCert *x509.PEMEncodedCertificateAndKey + + newCert, err := generate.NewAdminCertificateAndKey(time.Now(), t.talosCA, roles, certTTL) + if err != nil { + return nil, err + } + + newTalosconfig := clientconfig.NewConfig(talosconfigContextName, []string{endpoint}, t.talosCA.Crt, newCert) + + return newTalosconfig.Bytes() +} diff --git a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go index 11ae1a752..42ce82f6f 100644 --- a/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go +++ b/internal/app/machined/pkg/runtime/v1alpha2/v1alpha2_controller.go @@ -145,6 +145,7 @@ func (ctrl *Controller) Run(ctx context.Context, drainer *runtime.Drainer) error &k8s.StaticPodConfigController{}, &kubeaccess.ConfigController{}, &kubeaccess.EndpointController{}, + &kubeaccess.CRDController{}, &kubespan.ConfigController{}, &kubespan.EndpointController{}, &kubespan.IdentityController{}, diff --git a/internal/integration/api/serviceaccount.go b/internal/integration/api/serviceaccount.go new file mode 100644 index 000000000..e6d0651cb --- /dev/null +++ b/internal/integration/api/serviceaccount.go @@ -0,0 +1,303 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build integration_api +// +build integration_api + +package api + +import ( + "context" + "fmt" + "time" + + "github.com/siderolabs/go-pointer" + "github.com/talos-systems/go-retry/retry" + corev1 "k8s.io/api/core/v1" + eventsv1 "k8s.io/api/events/v1" + kubeerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/talos-systems/talos/internal/integration/base" + machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine" + "github.com/talos-systems/talos/pkg/machinery/client" + "github.com/talos-systems/talos/pkg/machinery/client/config" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1" + "github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine" + "github.com/talos-systems/talos/pkg/machinery/constants" +) + +var ( + serviceAccountGVR = schema.GroupVersionResource{ + Group: constants.ServiceAccountResourceGroup, + Version: constants.ServiceAccountResourceVersion, + Resource: constants.ServiceAccountResourcePlural, + } + secretGVR = schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "secrets", + } +) + +// ServiceAccountSuite verifies Talos ServiceAccount. +type ServiceAccountSuite struct { + base.K8sSuite + + ctx context.Context //nolint:containedctx + ctxCancel context.CancelFunc +} + +// SuiteName ... +func (suite *ServiceAccountSuite) SuiteName() string { + return "api.ServiceAccountSuite" +} + +// SetupTest ... +func (suite *ServiceAccountSuite) SetupTest() { + // make sure API calls have timeout + suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 5*time.Minute) +} + +// TearDownTest ... +func (suite *ServiceAccountSuite) TearDownTest() { + if suite.ctxCancel != nil { + suite.ctxCancel() + } +} + +// TestValid tests Kubernetes service accounts. +// +//nolint:dupl +func (suite *ServiceAccountSuite) TestValid() { + name := "test-valid" + + err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"}) + suite.Assert().NoError(err) + + _, err = suite.getCRD() + suite.Assert().NoError(err) + + sa, err := suite.createServiceAccount("kube-system", name, []string{"os:reader"}) + suite.Assert().NoError(err) + + defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "default", name) // nolint:errcheck + + err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool { + return event.Regarding.UID == sa.GetUID() && + event.Type == corev1.EventTypeNormal && + event.Reason == "Synced" + }) + suite.Assert().NoError(err) + + secret, err := suite.waitForSecret("kube-system", name) + suite.Assert().NoError(err) + + talosConfig := secret.Data["config"] + + conf, err := config.FromBytes(talosConfig) + suite.Assert().NoError(err) + + expectedServiceName := fmt.Sprintf( + "%s.%s", + constants.KubernetesTalosAPIServiceName, + constants.KubernetesTalosAPIServiceNamespace, + ) + suite.Assert().Equal([]string{expectedServiceName}, conf.Contexts[conf.Context].Endpoints) + + err = suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name) + suite.Require().NoError(err) + + err = suite.EnsureResourceIsDeleted(suite.ctx, 30*time.Second, secretGVR, "kube-system", name) + suite.Assert().NoError(err) +} + +// TestNotAllowedNamespace tests Kubernetes service accounts in not allowed namespaces. +// +//nolint:dupl +func (suite *ServiceAccountSuite) TestNotAllowedNamespace() { + name := "test-allowed-ns" + + err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"}) + suite.Assert().NoError(err) + + sa, err := suite.createServiceAccount("default", name, []string{"os:reader"}) + suite.Assert().NoError(err) + + defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "default", name) // nolint:errcheck + + err = suite.WaitForEventExists(suite.ctx, "default", func(event eventsv1.Event) bool { + return event.Regarding.UID == sa.GetUID() && + event.Type == corev1.EventTypeWarning && + event.Reason == "ErrNamespaceNotAllowed" + }) + suite.Assert().NoError(err) +} + +// TestNotAllowedRoles tests Kubernetes service accounts with not allowed roles. +// +//nolint:dupl +func (suite *ServiceAccountSuite) TestNotAllowedRoles() { + name := "test-not-allowed-roles" + + err := suite.configureAPIAccess(true, []string{"os:reader"}, []string{"kube-system"}) + suite.Assert().NoError(err) + + sa, err := suite.createServiceAccount("kube-system", name, []string{"os:admin"}) + suite.Assert().NoError(err) + + defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name) // nolint:errcheck + + err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool { + return event.Regarding.UID == sa.GetUID() && + event.Type == corev1.EventTypeWarning && + event.Reason == "ErrRolesNotAllowed" + }) + suite.Assert().NoError(err) +} + +// TestFeatureNotEnabled tests Kubernetes service accounts when API access feature is not enabled. +// +//nolint:dupl +func (suite *ServiceAccountSuite) TestFeatureNotEnabled() { + name := "test-feature-not-enabled" + + err := suite.configureAPIAccess(false, []string{"os:reader"}, []string{"kube-system"}) + suite.Assert().NoError(err) + + sa, err := suite.createServiceAccount("kube-system", name, []string{"os:reader"}) + if kubeerrors.IsNotFound(err) { + // CRD is not created because the feature was never enabled, all good + return + } + + suite.Assert().NoError(err) + + defer suite.DeleteResource(suite.ctx, serviceAccountGVR, "kube-system", name) // nolint:errcheck + + err = suite.WaitForEventExists(suite.ctx, "kube-system", func(event eventsv1.Event) bool { + return event.Regarding.UID == sa.GetUID() && + event.Type == corev1.EventTypeWarning && + event.Reason == "ErrAccessNotEnabled" + }) + + suite.Assert().NoError(err) +} + +func (suite *ServiceAccountSuite) waitForSecret(ns, name string) (*corev1.Secret, error) { + var ( + secret *corev1.Secret + err error + ) + + err = retry.Constant(1*time.Minute).RetryWithContext(suite.ctx, func(ctx context.Context) error { + secret, err = suite.Clientset.CoreV1().Secrets(ns).Get(suite.ctx, name, metav1.GetOptions{}) + if kubeerrors.IsNotFound(err) { + return retry.ExpectedError(err) + } + + return err + }) + if err != nil { + return nil, err + } + + return secret, nil +} + +func (suite *ServiceAccountSuite) getCRD() (*unstructured.Unstructured, error) { + crdName := fmt.Sprintf("%s.%s", constants.ServiceAccountResourcePlural, constants.ServiceAccountResourceGroup) + + return suite.DynamicClient.Resource(schema.GroupVersionResource{ + Group: "apiextensions.k8s.io", + Version: "v1", + Resource: "customresourcedefinitions", + }).Get(suite.ctx, crdName, metav1.GetOptions{}) +} + +func (suite *ServiceAccountSuite) createServiceAccount(ns string, name string, roles []string) (*unstructured.Unstructured, error) { + return suite.DynamicClient.Resource(serviceAccountGVR).Namespace(ns).Create(suite.ctx, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": fmt.Sprintf("%s/%s", constants.ServiceAccountResourceGroup, constants.ServiceAccountResourceVersion), + "kind": constants.ServiceAccountResourceKind, + "metadata": map[string]interface{}{ + "name": name, + }, + "spec": map[string]interface{}{ + "roles": roles, + }, + }, + }, metav1.CreateOptions{}) +} + +// configureAPIAccess configures the API access feature on all control plane nodes. +func (suite *ServiceAccountSuite) configureAPIAccess( + enabled bool, + allowedRoles []string, + allowedNamespaces []string, +) error { + controlPlaneIPs := suite.DiscoverNodeInternalIPsByType(suite.ctx, machine.TypeControlPlane) + + for _, ip := range controlPlaneIPs { + nodeCtx := client.WithNodes(suite.ctx, ip) + + nodeConfig, err := suite.ReadConfigFromNode(nodeCtx) + if err != nil { + return err + } + + nodeConfigRaw, ok := nodeConfig.Raw().(*v1alpha1.Config) + if !ok { + return fmt.Errorf("unexpected node config type %T", nodeConfig.Raw()) + } + + accessConfig := v1alpha1.KubernetesTalosAPIAccessConfig{ + AccessEnabled: pointer.To(enabled), + AccessAllowedRoles: allowedRoles, + AccessAllowedKubernetesNamespaces: allowedNamespaces, + } + + nodeConfigRaw.MachineConfig.MachineFeatures.KubernetesTalosAPIAccessConfig = &accessConfig + + bytes, err := nodeConfigRaw.Bytes() + if err != nil { + return err + } + + _, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{ + Data: bytes, + Mode: machineapi.ApplyConfigurationRequest_NO_REBOOT, + }) + + if err != nil { + return err + } + } + + if enabled { // wait for CRD and the Talos endpoint to be created + return retry.Constant(30*time.Second).RetryWithContext(suite.ctx, func(ctx context.Context) error { + _, err := suite.getCRD() + if err != nil { + return retry.ExpectedError(err) + } + + _, err = suite.Clientset.CoreV1(). + Services(constants.KubernetesTalosAPIServiceNamespace). + Get(suite.ctx, constants.KubernetesTalosAPIServiceName, metav1.GetOptions{}) + if err != nil { + return retry.ExpectedError(err) + } + + return nil + }) + } + + return nil +} + +func init() { + allSuites = append(allSuites, new(ServiceAccountSuite)) +} diff --git a/internal/integration/base/k8s.go b/internal/integration/base/k8s.go index 90eba044a..9090d74fa 100644 --- a/internal/integration/base/k8s.go +++ b/internal/integration/base/k8s.go @@ -14,12 +14,17 @@ import ( "github.com/talos-systems/go-retry/retry" corev1 "k8s.io/api/core/v1" + eventsv1 "k8s.io/api/events/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/talos-systems/talos/pkg/machinery/generic/slices" ) // K8sSuite is a base suite for K8s tests. @@ -27,6 +32,7 @@ type K8sSuite struct { APISuite Clientset *kubernetes.Clientset + DynamicClient dynamic.Interface DiscoveryClient *discovery.DiscoveryClient } @@ -51,6 +57,9 @@ func (k8sSuite *K8sSuite) SetupSuite() { k8sSuite.Clientset, err = kubernetes.NewForConfig(config) k8sSuite.Require().NoError(err) + k8sSuite.DynamicClient, err = dynamic.NewForConfig(config) + k8sSuite.Require().NoError(err) + k8sSuite.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(config) k8sSuite.Require().NoError(err) } @@ -111,3 +120,49 @@ func (k8sSuite *K8sSuite) GetK8sNodeReadinessStatus(ctx context.Context, nodeNam return "", fmt.Errorf("node %s has no readiness condition", nodeName) } + +// DeleteResource deletes the resource with the given GroupVersionResource, namespace and name. +// Does not return an error if the resource is not found. +func (k8sSuite *K8sSuite) DeleteResource(ctx context.Context, gvr schema.GroupVersionResource, ns, name string) error { + err := k8sSuite.DynamicClient.Resource(gvr).Namespace(ns).Delete(ctx, name, metav1.DeleteOptions{}) + if errors.IsNotFound(err) { + return nil + } + + return err +} + +// EnsureResourceIsDeleted ensures that the resource with the given GroupVersionResource, namespace and name does not exist on Kubernetes. +// It repeatedly checks the resource for the given duration. +func (k8sSuite *K8sSuite) EnsureResourceIsDeleted( + ctx context.Context, + duration time.Duration, + gvr schema.GroupVersionResource, + ns, name string, +) error { + return retry.Constant(duration).RetryWithContext(ctx, func(ctx context.Context) error { + _, err := k8sSuite.DynamicClient.Resource(gvr).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return nil + } + + return err + }) +} + +// WaitForEventExists waits for the event with the given namespace and check condition to exist on Kubernetes. +func (k8sSuite *K8sSuite) WaitForEventExists(ctx context.Context, ns string, checkFn func(event eventsv1.Event) bool) error { + return retry.Constant(15*time.Second).RetryWithContext(ctx, func(ctx context.Context) error { + events, err := k8sSuite.Clientset.EventsV1().Events(ns).List(ctx, metav1.ListOptions{}) + + filteredEvents := slices.Filter(events.Items, func(item eventsv1.Event) bool { + return checkFn(item) + }) + + if len(filteredEvents) == 0 { + return retry.ExpectedError(err) + } + + return nil + }) +} diff --git a/internal/integration/cli/inject.go b/internal/integration/cli/inject.go new file mode 100644 index 000000000..2219acd36 --- /dev/null +++ b/internal/integration/cli/inject.go @@ -0,0 +1,95 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +//go:build integration_cli +// +build integration_cli + +package cli + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" + + "github.com/talos-systems/talos/internal/integration/base" +) + +var ( + //go:embed testdata/inject/talosconfig-input.yaml + inputManifests []byte + + //go:embed testdata/inject/talosconfig-expected.yaml + expectedManifests []byte +) + +// InjectSuite verifies inject command. +type InjectSuite struct { + base.CLISuite +} + +// SuiteName ... +func (suite *InjectSuite) SuiteName() string { + return "cli.InjectSuite" +} + +// TestServiceAccount tests inject serviceaccount command. +func (suite *InjectSuite) TestServiceAccount() { + suite.testServiceAccount(inputManifests) +} + +// TestServiceAccountAlreadyInjectedNoChange tests inject serviceaccount command when the input manifest is already injected, +// makes sure that it stays the same. +func (suite *InjectSuite) TestServiceAccountAlreadyInjectedNoChange() { + suite.testServiceAccount(expectedManifests) +} + +func (suite *InjectSuite) testServiceAccount(input []byte) { + expectedDocs, err := yamlDocs(expectedManifests) + suite.Require().NoError(err) + + tempDir := suite.T().TempDir() + + inputPath := filepath.Join(tempDir, "input.yaml") + + err = os.WriteFile(inputPath, input, 0o644) + suite.Require().NoError(err) + + stdout, _ := suite.RunCLI([]string{"inject", "serviceaccount", "-f", inputPath, "--roles", "os:reader,os:admin"}) + + stdoutDocs, err := yamlDocs([]byte(stdout)) + suite.Require().NoError(err) + + suite.Assert().Equal(expectedDocs, stdoutDocs, "inject serviceaccount output did not match expected output") +} + +func yamlDocs(input []byte) ([]map[string]any, error) { + decoder := yaml.NewDecoder(bytes.NewReader(input)) + + var docs []map[string]any + + for { + var doc map[string]any + + if err := decoder.Decode(&doc); err != nil { + if err == io.EOF { + break + } + + return nil, fmt.Errorf("document decode failed: %w", err) + } + + docs = append(docs, doc) + } + + return docs, nil +} + +func init() { + allSuites = append(allSuites, new(InjectSuite)) +} diff --git a/internal/integration/cli/testdata/inject/talosconfig-expected.yaml b/internal/integration/cli/testdata/inject/talosconfig-expected.yaml new file mode 100644 index 000000000..91aa8366d --- /dev/null +++ b/internal/integration/cli/testdata/inject/talosconfig-expected.yaml @@ -0,0 +1,302 @@ +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: donottouch +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null + name: test1 +spec: + containers: + - env: + - name: TEST + value: test + image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + initContainers: + - image: busybox + name: init1 + resources: {} + volumeMounts: + - mountPath: /tmp/hello + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: busybox + name: init2 + resources: {} + volumeMounts: + - mountPath: /tmp/hello + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - emptyDir: {} + name: vol1 + - name: talos-secrets + secret: + secretName: test1-talos-secrets +status: {} +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: test1-talos-secrets +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + name: test1 +spec: + selector: + matchLabels: + app: test + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: test + spec: + containers: + - image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - emptyDir: {} + name: vol1 + - name: talos-secrets + secret: + secretName: test1-talos-secrets +status: {} +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + creationTimestamp: null + name: test2 +spec: + selector: + matchLabels: + app: test + template: + metadata: + creationTimestamp: null + labels: + app: test + spec: + containers: + - image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - emptyDir: {} + name: vol1 + - name: talos-secrets + secret: + secretName: test2-talos-secrets + updateStrategy: {} +status: + currentNumberScheduled: 0 + desiredNumberScheduled: 0 + numberMisscheduled: 0 + numberReady: 0 +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: test2-talos-secrets +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + creationTimestamp: null + name: test3 +spec: + selector: + matchLabels: + app: test + serviceName: test + template: + metadata: + creationTimestamp: null + labels: + app: test + spec: + containers: + - image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - emptyDir: {} + name: vol1 + - name: talos-secrets + secret: + secretName: test3-talos-secrets + updateStrategy: {} +status: + availableReplicas: 0 + replicas: 0 +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: test3-talos-secrets +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + creationTimestamp: null + name: test4 + namespace: testns +spec: + jobTemplate: + metadata: + creationTimestamp: null + labels: + app: test + spec: + template: + metadata: + creationTimestamp: null + labels: + app: test + spec: + containers: + - image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - name: talos-secrets + secret: + secretName: test4-talos-secrets + schedule: '*/1 * * * *' +status: {} +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: test4-talos-secrets + namespace: testns +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: batch/v1 +kind: Job +metadata: + creationTimestamp: null + name: test5 + namespace: testns2 +spec: + template: + metadata: + creationTimestamp: null + spec: + containers: + - image: alpine:3 + name: container1 + resources: {} + volumeMounts: + - mountPath: /mnt/vol1 + name: vol1 + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + - image: alpine:3 + name: container2 + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + volumes: + - name: talos-secrets + secret: + secretName: test5-talos-secrets +status: {} +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: test5-talos-secrets + namespace: testns2 +spec: + roles: + - os:reader + - os:admin +--- diff --git a/internal/integration/cli/testdata/inject/talosconfig-input.yaml b/internal/integration/cli/testdata/inject/talosconfig-input.yaml new file mode 100644 index 000000000..09218aff0 --- /dev/null +++ b/internal/integration/cli/testdata/inject/talosconfig-input.yaml @@ -0,0 +1,159 @@ +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: donottouch +spec: + roles: + - os:reader + - os:admin +--- +apiVersion: v1 +kind: Pod +metadata: + name: test1 +spec: + volumes: + - name: vol1 + emptyDir: {} + initContainers: + - name: init1 + image: busybox + volumeMounts: + - name: vol1 + mountPath: /tmp/hello + - name: init2 + image: busybox + volumeMounts: + - name: vol1 + mountPath: /tmp/hello + containers: + - name: container1 + image: alpine:3 + env: + - name: TEST + value: test + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test1 +spec: + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + volumes: + - name: vol1 + emptyDir: {} + containers: + - name: container1 + image: alpine:3 + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: test2 +spec: + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + volumes: + - name: vol1 + emptyDir: {} + containers: + - name: container1 + image: alpine:3 + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: test3 +spec: + serviceName: test + selector: + matchLabels: + app: test + template: + metadata: + labels: + app: test + spec: + volumes: + - name: vol1 + emptyDir: {} + containers: + - name: container1 + image: alpine:3 + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: test4 + namespace: testns +spec: + schedule: "*/1 * * * *" + jobTemplate: + metadata: + labels: + app: test + spec: + template: + metadata: + labels: + app: test + spec: + containers: + - name: container1 + image: alpine:3 + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: test5 + namespace: testns2 +spec: + template: + spec: + containers: + - name: container1 + image: alpine:3 + volumeMounts: + - name: vol1 + mountPath: /mnt/vol1 + - name: container2 + image: alpine:3 diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index ce39db9d9..19fb5034b 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -11,6 +11,7 @@ package integration_test import ( "context" "flag" + "fmt" "os" "path/filepath" "testing" @@ -120,7 +121,7 @@ func TestIntegration(t *testing.T) { } func init() { - defaultTalosConfig, _ := clientconfig.GetDefaultPath() //nolint:errcheck + defaultTalosConfigs, _ := clientconfig.GetDefaultPaths() //nolint:errcheck defaultStateDir, err := clientconfig.GetTalosDirectory() if err == nil { @@ -130,7 +131,16 @@ func init() { flag.BoolVar(&failFast, "talos.failfast", false, "fail the test run on the first failed test") flag.BoolVar(&crashdumpEnabled, "talos.crashdump", true, "print crashdump on test failure (only if provisioner is enabled)") - flag.StringVar(&talosConfig, "talos.config", defaultTalosConfig, "The path to the Talos configuration file") + flag.StringVar( + &talosConfig, + "talos.config", + defaultTalosConfigs[0].Path, + fmt.Sprintf("The path to the Talos configuration file. Defaults to '%s' env variable if set, otherwise '%s' and '%s' in order.", + constants.TalosConfigEnvVar, + filepath.Join("$HOME", constants.TalosDir, constants.TalosconfigFilename), + filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename), + ), + ) flag.StringVar(&endpoint, "talos.endpoint", "", "endpoint to use (overrides config)") flag.StringVar(&k8sEndpoint, "talos.k8sendpoint", "", "Kubernetes endpoint to use (overrides kubeconfig)") flag.StringVar(&provisionerName, "talos.provisioner", "", "Talos cluster provisioner to use, if not set cluster state is disabled") diff --git a/internal/integration/provision/upgrade.go b/internal/integration/provision/upgrade.go index 379fcc03a..f729d474d 100644 --- a/internal/integration/provision/upgrade.go +++ b/internal/integration/provision/upgrade.go @@ -462,15 +462,12 @@ func (suite *UpgradeSuite) setupCluster() { ) suite.Require().NoError(err) - defaultTalosConfig, err := clientconfig.GetDefaultPath() - suite.Require().NoError(err) - - c, err := clientconfig.Open(defaultTalosConfig) + c, err := clientconfig.Open("") suite.Require().NoError(err) c.Merge(suite.configBundle.TalosConfig()) - suite.Require().NoError(c.Save(defaultTalosConfig)) + suite.Require().NoError(c.Save("")) suite.clusterAccess = access.NewAdapter(suite.Cluster, provision.WithTalosConfig(suite.configBundle.TalosConfig())) diff --git a/internal/pkg/etcd/lock.go b/internal/pkg/etcd/lock.go new file mode 100644 index 000000000..08f5c29f2 --- /dev/null +++ b/internal/pkg/etcd/lock.go @@ -0,0 +1,44 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package etcd + +import ( + "context" + "fmt" + + "go.etcd.io/etcd/client/v3/concurrency" + "go.uber.org/zap" +) + +// WithLock executes the given function exclusively by acquiring an Etcd lock with the given key. +func WithLock(ctx context.Context, key string, logger *zap.Logger, f func() error) error { + etcdClient, err := NewLocalClient() + if err != nil { + return fmt.Errorf("error creating etcd client: %w", err) + } + + defer etcdClient.Close() //nolint:errcheck + + session, err := concurrency.NewSession(etcdClient.Client) + if err != nil { + return fmt.Errorf("error creating etcd session: %w", err) + } + + defer session.Close() //nolint:errcheck + + mutex := concurrency.NewMutex(session, key) + + logger.Debug("waiting for mutex", zap.String("key", key)) + + if err = mutex.Lock(ctx); err != nil { + return fmt.Errorf("error acquiring mutex for key %s: %w", key, err) + } + + logger.Debug("mutex acquired", zap.String("key", key)) + + defer mutex.Unlock(ctx) //nolint:errcheck + + return f() +} diff --git a/internal/pkg/tui/installer/installer.go b/internal/pkg/tui/installer/installer.go index 465373b70..6b9a776ac 100644 --- a/internal/pkg/tui/installer/installer.go +++ b/internal/pkg/tui/installer/installer.go @@ -8,7 +8,6 @@ package installer import ( "context" "fmt" - "os" "strings" "sync" @@ -427,26 +426,11 @@ func (installer *Installer) apply(conn *Connection) error { } func (installer *Installer) writeTalosconfig(list *tview.Flex, talosconfig *clientconfig.Config) error { - path, err := clientconfig.GetDefaultPath() + config, err := clientconfig.Open("") if err != nil { return err } - f, err := os.Open(path) - - var config *clientconfig.Config - - if err != nil && !os.IsNotExist(err) { - return err - } - - if err == nil { - config, err = clientconfig.ReadFrom(f) - if err != nil { - return err - } - } - text := tview.NewTextView() addLines := func(lines ...string) { t := text.GetText(false) @@ -457,17 +441,12 @@ func (installer *Installer) writeTalosconfig(list *tview.Flex, talosconfig *clie addLines( "", - fmt.Sprintf("Merging talosconfig into %s...", path), + fmt.Sprintf("Merging talosconfig into %s...", config.Path().Path), ) text.SetBackgroundColor(color) list.AddItem(text, 0, 1, false) - renames := []clientconfig.Rename{} - if config != nil { - renames = config.Merge(talosconfig) - } else { - config = talosconfig - } + renames := config.Merge(talosconfig) for _, rename := range renames { addLines(fmt.Sprintf("Renamed %s.", rename.String())) @@ -481,7 +460,7 @@ func (installer *Installer) writeTalosconfig(list *tview.Flex, talosconfig *clie config.Context = context addLines(fmt.Sprintf("Set current context to %q.", context)) - err = config.Save(path) + err = config.Save("") if err != nil { return err } diff --git a/pkg/kubernetes/inject/serviceaccount.go b/pkg/kubernetes/inject/serviceaccount.go new file mode 100644 index 000000000..945bf8cd7 --- /dev/null +++ b/pkg/kubernetes/inject/serviceaccount.go @@ -0,0 +1,378 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package inject + +import ( + "bytes" + "errors" + "fmt" + "io" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + + "github.com/talos-systems/talos/pkg/machinery/constants" +) + +const ( + injectToEnv = false + volumeName = "talos-secrets" + + nameSuffix = "-talos-secrets" + + apiVersionField = "apiVersion" + kindField = "kind" + metadataField = "metadata" + namespaceField = "namespace" + nameField = "name" + + yamlSeparator = "---\n" +) + +// ServiceAccount takes a YAML with Kubernetes manifests and requested Talos roles as input +// and injects Talos service accounts into them. +// +//nolint:gocyclo +func ServiceAccount(reader io.Reader, roles []string) ([]byte, error) { + var err error + + objectSerializer := json.NewSerializerWithOptions( + json.DefaultMetaFactory, + nil, + nil, + json.SerializerOptions{ + Yaml: true, + Pretty: true, + Strict: true, + }, + ) + + seenResourceIDs := make(map[string]struct{}) + + var buf bytes.Buffer + + decoder := yaml.NewDecoder(reader) + + // loop over all documents in a possibly YAML with multiple documents separated by --- + for { + var raw map[string]any + + err = decoder.Decode(&raw) + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, err + } + + if raw == nil { + continue + } + + var injected metav1.Object + + injected, err = injectToObject(raw) + if err != nil { // not a known resource with a PodSpec + // if this is already a Talos ServiceAccount resource we have seen, + // we keep it only if we have not seen it yet (means it belongs to the user, not injected by us) + id := readResourceIDFromServiceAccount(raw) + if id != "" { + if _, ok := seenResourceIDs[id]; ok { + continue + } + + seenResourceIDs[id] = struct{}{} + } + + err = yaml.NewEncoder(&buf).Encode(raw) + if err != nil { + return nil, err + } + + buf.WriteString(yamlSeparator) + + continue + } + + // injectable resource type which contains a PodSpec + + runtimeObject, ok := injected.(runtime.Object) + if !ok { + return nil, errors.New("injected object is not a runtime.Object") + } + + err = objectSerializer.Encode(runtimeObject, &buf) + if err != nil { + return nil, err + } + + buf.WriteString(yamlSeparator) + + id := readResourceIDFromObject(injected) + + // inject service account for the resource + if _, ok = seenResourceIDs[id]; !ok { + sa := buildServiceAccount(injected.GetNamespace(), fmt.Sprintf("%s%s", injected.GetName(), nameSuffix), roles) + + err = yaml.NewEncoder(&buf).Encode(sa) + if err != nil { + return nil, err + } + + buf.WriteString(yamlSeparator) + + // mark resource as seen + seenResourceIDs[id] = struct{}{} + } + } + + return buf.Bytes(), nil +} + +func buildServiceAccount(namespace string, name string, roles []string) map[string]any { + metadata := map[string]any{ + nameField: name, + } + + if namespace != "" { + metadata[namespaceField] = namespace + } + + return map[string]any{ + apiVersionField: fmt.Sprintf( + "%s/%s", + constants.ServiceAccountResourceGroup, + constants.ServiceAccountResourceVersion, + ), + kindField: constants.ServiceAccountResourceKind, + metadataField: metadata, + "spec": map[string]any{ + "roles": roles, + }, + } +} + +func isServiceAccount(raw map[string]any) bool { + apiVersionKind, err := readResourceAPIVersionKind(raw) + if err != nil { + return false + } + + return apiVersionKind == fmt.Sprintf( + "%s/%s/%s", + constants.ServiceAccountResourceGroup, + constants.ServiceAccountResourceVersion, + constants.ServiceAccountResourceKind, + ) +} + +// injectToDocument takes a single YAML document and attempts to inject a ServiceAccount +// into it if it is a known Kubernetes resource type which contains a corev1.PodSpec. +// +//nolint:gocyclo +func injectToObject(raw map[string]any) (metav1.Object, error) { + var err error + + apiVersionKind, err := readResourceAPIVersionKind(raw) + if err != nil { + return nil, err + } + + switch apiVersionKind { + case "v1/Pod": + return injectToPodSpecObject[corev1.Pod](raw, func(obj *corev1.Pod) *corev1.PodSpec { + return &obj.Spec + }) + + case "apps/v1/Deployment": + return injectToPodSpecObject[appsv1.Deployment](raw, func(obj *appsv1.Deployment) *corev1.PodSpec { + return &obj.Spec.Template.Spec + }) + + case "apps/v1/StatefulSet": + return injectToPodSpecObject[appsv1.StatefulSet](raw, func(obj *appsv1.StatefulSet) *corev1.PodSpec { + return &obj.Spec.Template.Spec + }) + + case "apps/v1/DaemonSet": + return injectToPodSpecObject[appsv1.DaemonSet](raw, func(obj *appsv1.DaemonSet) *corev1.PodSpec { + return &obj.Spec.Template.Spec + }) + + case "batch/v1/Job": + return injectToPodSpecObject[batchv1.Job](raw, func(obj *batchv1.Job) *corev1.PodSpec { + return &obj.Spec.Template.Spec + }) + + case "batch/v1/CronJob": + return injectToPodSpecObject[batchv1.CronJob](raw, func(obj *batchv1.CronJob) *corev1.PodSpec { + return &obj.Spec.JobTemplate.Spec.Template.Spec + }) + } + + return nil, fmt.Errorf("unsupported object type: %s", apiVersionKind) +} + +func injectToPodSpecObject[T any](raw map[string]any, podSpecFunc func(*T) *corev1.PodSpec) (*T, error) { + objectName, nameFound, err := unstructured.NestedString(raw, metadataField, nameField) + if err != nil { + return nil, err + } + + if !nameFound { + return nil, errors.New("object has no name") + } + + var obj T + + err = runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(raw, &obj, false) + if err != nil { + return nil, err + } + + injectToPodSpec(fmt.Sprintf("%s%s", objectName, nameSuffix), podSpecFunc(&obj)) + + return &obj, nil +} + +func readResourceAPIVersionKind(raw map[string]any) (string, error) { + apiVersion, found, err := unstructured.NestedString(raw, apiVersionField) + if err != nil { + return "", err + } + + if !found { + return "", fmt.Errorf("%s not found", apiVersionField) + } + + kind, found, err := unstructured.NestedString(raw, kindField) + if err != nil { + return "", err + } + + if !found { + return "", fmt.Errorf("%s not found", kindField) + } + + return fmt.Sprintf("%s/%s", apiVersion, kind), nil +} + +func readResourceIDFromObject(obj metav1.Object) string { + if obj.GetNamespace() == "" { + return obj.GetName() + } + + return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()) +} + +func readResourceIDFromServiceAccount(raw map[string]any) string { + if !isServiceAccount(raw) { + return "" + } + + name, nameFound, err := unstructured.NestedString(raw, metadataField, nameField) + if err != nil || !nameFound { + return "" + } + + nameTrimmed := strings.TrimSuffix(name, nameSuffix) + + ns, nsFound, err := unstructured.NestedString(raw, metadataField, namespaceField) + if err != nil { + return "" + } + + if nsFound { + return fmt.Sprintf("%s/%s", ns, nameTrimmed) + } + + return nameTrimmed +} + +func injectToPodSpec(secretName string, podSpec *corev1.PodSpec) { + podSpec.Volumes = injectToVolumes(secretName, podSpec.Volumes) + podSpec.InitContainers = injectToContainers(podSpec.InitContainers) + podSpec.Containers = injectToContainers(podSpec.Containers) +} + +func injectToVolumes(name string, volumes []corev1.Volume) []corev1.Volume { + result := make([]corev1.Volume, 0, len(volumes)) + + for _, volume := range volumes { + if volume.Name != volumeName { + result = append(result, volume) + } + } + + result = append(result, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: name, + }, + }, + }) + + return result +} + +func injectToContainers(containers []corev1.Container) []corev1.Container { + result := make([]corev1.Container, 0, len(containers)) + + for _, container := range containers { + injectToContainer(&container) + + result = append(result, container) + } + + return result +} + +func injectToContainer(container *corev1.Container) { + volumeMounts := make([]corev1.VolumeMount, 0, len(container.VolumeMounts)) + + for _, mount := range container.VolumeMounts { + if mount.Name != volumeName { + volumeMounts = append(volumeMounts, mount) + } + } + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: constants.ServiceAccountMountPath, + }) + + container.VolumeMounts = volumeMounts + + if injectToEnv { + container.Env = injectToContainerEnv(container.Env) + } +} + +func injectToContainerEnv(env []corev1.EnvVar) []corev1.EnvVar { + result := make([]corev1.EnvVar, 0, len(env)) + + for _, envVar := range env { + if envVar.Name != constants.TalosConfigEnvVar { + result = append(result, envVar) + } + } + + result = append(result, corev1.EnvVar{ + Name: constants.TalosConfigEnvVar, + Value: filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename), + }) + + return result +} diff --git a/pkg/machinery/client/config/config.go b/pkg/machinery/client/config/config.go index 78c13da7d..6e5348449 100644 --- a/pkg/machinery/client/config/config.go +++ b/pkg/machinery/client/config/config.go @@ -13,13 +13,16 @@ import ( "path/filepath" "github.com/talos-systems/crypto/x509" - yaml "gopkg.in/yaml.v3" + "gopkg.in/yaml.v3" ) // Config represents the client configuration file (talosconfig). type Config struct { Context string `yaml:"context"` Contexts map[string]*Context `yaml:"contexts"` + + // path is the config Path config is read from. + path Path } // NewConfig returns the client configuration file with a single context. @@ -61,21 +64,50 @@ func (c *Context) upgrade() { } // Open reads the config and initializes a Config struct. -func Open(p string) (c *Config, err error) { - if err = ensure(p); err != nil { +// If path is explicitly set, it will be used. +// If not, the default path rules will be used. +func Open(path string) (*Config, error) { + var ( + confPath Path + err error + ) + + if path != "" { // path is explicitly specified, ensure that is created and use it + confPath = Path{ + Path: path, + WriteAllowed: true, + } + + err = ensure(confPath.Path) + if err != nil { + return nil, err + } + } else { // path is implicit, get the first already existing & readable path or ensure that it is created + confPath, err = firstValidPath() + if err != nil { + return nil, err + } + } + + config, err := fromFile(confPath.Path) + if err != nil { return nil, err } - var f *os.File + config.path = confPath - f, err = os.Open(p) + return config, nil +} + +func fromFile(path string) (*Config, error) { + file, err := os.Open(path) if err != nil { - return + return nil, err } - defer f.Close() //nolint:errcheck + defer file.Close() //nolint:errcheck - return ReadFrom(f) + return ReadFrom(file) } // FromString returns a config from a string. @@ -102,18 +134,37 @@ func ReadFrom(r io.Reader) (c *Config, err error) { } // Save writes the config to disk. -func (c *Config) Save(p string) (err error) { - configBytes, err := c.Bytes() - if err != nil { - return +// If the path is not explicitly set, the default path rules will be used. +func (c *Config) Save(path string) error { + var err error + + if path != "" { // path is explicitly specified, use it + c.path = Path{ + Path: path, + WriteAllowed: true, + } + } else if c.path.Path == "" { // path is implicit and is not set on config, get the first already existing & writable path or create it + c.path, err = firstValidPath() + if err != nil { + return err + } } - if err = os.MkdirAll(filepath.Dir(p), 0o700); err != nil { + if !c.path.WriteAllowed { + return fmt.Errorf("not allowed to write to config: %s", c.path.Path) + } + + configBytes, err := c.Bytes() + if err != nil { return err } - if err = os.WriteFile(p, configBytes, 0o600); err != nil { - return + if err = os.MkdirAll(filepath.Dir(c.path.Path), 0o700); err != nil { + return err + } + + if err = os.WriteFile(c.path.Path, configBytes, 0o600); err != nil { + return err } return nil @@ -124,6 +175,11 @@ func (c *Config) Bytes() ([]byte, error) { return yaml.Marshal(c) } +// Path returns the filesystem path config was read from. +func (c *Config) Path() Path { + return c.path +} + // Rename describes context rename during merge. type Rename struct { From string @@ -175,14 +231,14 @@ func (c *Config) Merge(cfg *Config) []Rename { return renames } -func ensure(filename string) (err error) { - if _, err := os.Stat(filename); os.IsNotExist(err) { +func ensure(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { config := &Config{ Context: "", Contexts: map[string]*Context{}, } - return config.Save(filename) + return config.Save(path) } return nil diff --git a/pkg/machinery/client/config/path.go b/pkg/machinery/client/config/path.go index dbbe765ba..e2f558532 100644 --- a/pkg/machinery/client/config/path.go +++ b/pkg/machinery/client/config/path.go @@ -5,12 +5,21 @@ package config import ( + "fmt" "os" "path/filepath" "github.com/talos-systems/talos/pkg/machinery/constants" ) +// Path represents a path to a configuration file. +type Path struct { + // Path is the filesystem path of the config. + Path string + // WriteAllowed is true if the path is allowed to be written. + WriteAllowed bool +} + // GetTalosDirectory returns path to Talos directory (~/.talos). func GetTalosDirectory() (string, error) { home, err := os.UserHomeDir() @@ -18,30 +27,70 @@ func GetTalosDirectory() (string, error) { return "", err } - return filepath.Join(home, ".talos"), nil + return filepath.Join(home, constants.TalosDir), nil } -// GetDefaultPath returns default path to Talos config. -func GetDefaultPath() (string, error) { - if path, ok := os.LookupEnv(constants.TalosConfigEnvVar); ok { - return path, nil - } - - talosSAPath := filepath.Join(constants.ServiceAccountMountPath, constants.ServiceAccountTalosconfigFilename) - - _, err := os.Stat(talosSAPath) - if err != nil && !os.IsNotExist(err) && !os.IsPermission(err) { - return "", err - } - - if err == nil { - return talosSAPath, nil - } - +// GetDefaultPaths returns the list of config file paths in order of priority. +func GetDefaultPaths() ([]Path, error) { talosDir, err := GetTalosDirectory() if err != nil { - return "", err + return nil, err } - return filepath.Join(talosDir, "config"), nil + result := make([]Path, 0, 3) + + if path, ok := os.LookupEnv(constants.TalosConfigEnvVar); ok { + result = append(result, Path{ + Path: path, + WriteAllowed: true, + }) + } + + result = append( + result, + Path{ + Path: filepath.Join(talosDir, constants.TalosconfigFilename), + WriteAllowed: true, + }, + Path{ + Path: filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename), + WriteAllowed: false, + }, + ) + + return result, nil +} + +// firstValidPath iterates over the default paths and returns the first one that exists and readable. +// If no path is found, it will ensure that the first path that allows writes is created and returned. +// If no path is found that is writable, an error is returned. +func firstValidPath() (Path, error) { + paths, err := GetDefaultPaths() + if err != nil { + return Path{}, err + } + + var firstWriteAllowed Path + + for _, path := range paths { + _, err = os.Stat(path.Path) + if err == nil { + return path, nil + } + + if firstWriteAllowed.Path == "" && path.WriteAllowed { + firstWriteAllowed = path + } + } + + if firstWriteAllowed.Path == "" { + return Path{}, fmt.Errorf("no valid config paths found") + } + + err = ensure(firstWriteAllowed.Path) + if err != nil { + return Path{}, err + } + + return firstWriteAllowed, nil } diff --git a/pkg/machinery/client/options.go b/pkg/machinery/client/options.go index 279eec48c..9e775a6c1 100644 --- a/pkg/machinery/client/options.go +++ b/pkg/machinery/client/options.go @@ -92,12 +92,7 @@ func WithEndpoints(endpoints ...string) OptionFunc { // Additionally use WithContextName to select a context other than the default. func WithDefaultConfig() OptionFunc { return func(o *Options) (err error) { - defaultConfigPath, err := clientconfig.GetDefaultPath() - if err != nil { - return fmt.Errorf("no client configuration provided and no default path found: %w", err) - } - - return WithConfigFromFile(defaultConfigPath)(o) + return WithConfigFromFile("")(o) } } diff --git a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go index a91ae5714..780b90adb 100644 --- a/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go +++ b/pkg/machinery/config/types/v1alpha1/v1alpha1_validation.go @@ -438,7 +438,6 @@ func (c *ClusterDiscoveryConfig) Validate(clusterCfg *ClusterConfig) error { // ValidateNetworkDevices runs the specified validation checks specific to the // network devices. func ValidateNetworkDevices(d *Device, pairedInterfaces map[string]string, checks ...NetworkDeviceCheck) ([]string, error) { - // todo utku var result *multierror.Error if d == nil { diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index d3aa1a430..e65d2393e 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -349,9 +349,12 @@ const ( // EtcdTalosEtcdUpgradeMutex is the etcd mutex prefix to be used to set an etcd upgrade lock. EtcdTalosEtcdUpgradeMutex = EtcdRootTalosKey + ":etcdUpgradeMutex" - // EtcdTalosManifestApplyMutex is the etcd election . + // EtcdTalosManifestApplyMutex is the etcd mutex prefix used by manifest apply controller. EtcdTalosManifestApplyMutex = EtcdRootTalosKey + ":manifestApplyMutex" + // EtcdTalosServiceAccountCRDControllerMutex is the etcd mutex prefix used by Talos ServiceAccount crd controller. + EtcdTalosServiceAccountCRDControllerMutex = EtcdRootTalosKey + ":serviceAccountCRDController" + // EtcdImage is the reposistory for the etcd image. EtcdImage = "gcr.io/etcd-development/etcd" @@ -708,14 +711,35 @@ const ( // KubernetesTalosAPIServiceNamespace is the namespace of the Kubernetes service to access Talos API. KubernetesTalosAPIServiceNamespace = "default" + // TalosDir is the default name of the Talos directory under user home. + TalosDir = ".talos" + + // TalosconfigFilename is the file name of Talosconfig under TalosDir or under ServiceAccountMountPath inside a pod. + TalosconfigFilename = "config" + // KubernetesTalosProvider is the name of the Talos provider as a Kubernetes label. KubernetesTalosProvider = "talos.dev" - // ServiceAccountTalosconfigFilename is the file name of Talosconfig when it is injected into a pod. - ServiceAccountTalosconfigFilename = "config" + // ServiceAccountResourceGroup is the group name of the Talos service account CRD. + ServiceAccountResourceGroup = "talos.dev" + + // ServiceAccountResourceVersion is the version of the Talos service account CRD. + ServiceAccountResourceVersion = "v1alpha1" + + // ServiceAccountResourceKind is the kind name of the Talos service account CRD. + ServiceAccountResourceKind = "ServiceAccount" + + // ServiceAccountResourceSingular is the singular name of the Talos service account CRD. + ServiceAccountResourceSingular = "serviceaccount" + + // ServiceAccountResourceShortName is the short name of the service account CRD. + ServiceAccountResourceShortName = "tsa" + + // ServiceAccountResourcePlural is the plural name of the service account CRD. + ServiceAccountResourcePlural = ServiceAccountResourceSingular + "s" // ServiceAccountMountPath is the path of the directory in which the Talos service account secrets are mounted. - ServiceAccountMountPath = "/var/run/secrets/talos.dev/" + ServiceAccountMountPath = "/var/run/secrets/talos.dev" // DefaultTrustedCAFile is the default path to the trusted CA file. DefaultTrustedCAFile = "/etc/ssl/certs/ca-certificates" diff --git a/website/content/v1.2/advanced/talos-api-access-from-k8s.md b/website/content/v1.2/advanced/talos-api-access-from-k8s.md new file mode 100644 index 000000000..c14789e61 --- /dev/null +++ b/website/content/v1.2/advanced/talos-api-access-from-k8s.md @@ -0,0 +1,157 @@ +--- +title: "Talos API access from Kubernetes" +description: "How to access Talos API from within Kubernetes." +aliases: + - ../guides/talos-api-access-from-k8s +--- + +In this guide, we will enable the Talos feature to access the Talos API from within Kubernetes. + +## Enabling the Feature + +Edit the machine configuration to enable the feature, specifying the Kubernetes namespaces from which Talos API +can be accessed and the allowed Talos API roles. + +```bash +talosctl -n 172.20.0.2 edit machineconfig +``` + +Configure the `kubernetesTalosAPIAccess` like the following: + +```yaml +spec: + machine: + features: + kubernetesTalosAPIAccess: + enabled: true + allowedRoles: + - os:reader + allowedKubernetesNamespaces: + - default +``` + +## Injecting Talos ServiceAccount into manifests + +Create the following manifest file `deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: talos-api-access +spec: + selector: + matchLabels: + app: talos-api-access + template: + metadata: + labels: + app: talos-api-access + spec: + containers: + - name: talos-api-access + image: alpine:3 + command: + - sh + - -c + - | + wget -O /usr/local/bin/talosctl https://github.com/siderolabs/talos/releases/download//talosctl-linux-amd64 + chmod +x /usr/local/bin/talosctl + while true; talosctl -n 172.20.0.2 version; do sleep 1; done +``` + +**Note:** make sure that you replace the IP `172.20.0.2` with a valid Talos node IP. + +Use `talosctl inject serviceaccount` command to inject the Talos ServiceAccount into the manifest. + +```bash +talosctl inject serviceaccount -f deployment.yaml > deployment-injected.yaml +``` + +Inspect the generated manifest: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + creationTimestamp: null + name: talos-api-access +spec: + selector: + matchLabels: + app: talos-api-access + strategy: {} + template: + metadata: + creationTimestamp: null + labels: + app: talos-api-access + spec: + containers: + - command: + - sh + - -c + - | + wget -O /usr/local/bin/talosctl https://github.com/siderolabs/talos/releases/download//talosctl-linux-amd64 + chmod +x /usr/local/bin/talosctl + while true; talosctl -n 172.20.0.2 version; do sleep 1; done + image: alpine:3 + name: talos-api-access + resources: {} + volumeMounts: + - mountPath: /var/run/secrets/talos.dev + name: talos-secrets + tolerations: + - operator: Exists + volumes: + - name: talos-secrets + secret: + secretName: talos-api-access-talos-secrets +status: {} +--- +apiVersion: talos.dev/v1alpha1 +kind: ServiceAccount +metadata: + name: talos-api-access-talos-secrets +spec: + roles: + - os:reader +--- +``` + +As you can notice, your deployment manifest is now injected with the Talos ServiceAccount. + +## Testing API Access + +Apply the new manifest into `default` namespace: + +```bash +kubectl apply -n default -f deployment-injected.yaml +``` + +Follow the logs of the pods belong to the deployment: + +```bash +kubectl logs -n default -f -l app=talos-api-access +``` + +You'll see a repeating output similar to the following: + +```text +Client: + Tag: + SHA: .... + Built: + Go version: go1.18.4 + OS/Arch: linux/amd64 +Server: + NODE: 172.20.0.2 + Tag: + SHA: ... + Built: + Go version: go1.18.4 + OS/Arch: linux/amd64 + Enabled: RBAC +``` + +This means that the pod can talk to Talos API of node 172.20.0.2 successfully. diff --git a/website/content/v1.2/reference/cli.md b/website/content/v1.2/reference/cli.md index 951c79e91..cd8c14a15 100644 --- a/website/content/v1.2/reference/cli.md +++ b/website/content/v1.2/reference/cli.md @@ -31,7 +31,7 @@ talosctl apply-config [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -71,7 +71,7 @@ talosctl bootstrap [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -162,7 +162,7 @@ talosctl cluster create [flags] -n, --nodes strings target the specified nodes --provisioner string Talos cluster provisioner to use (default "docker") --state string directory path to store cluster state (default "/home/user/.talos/clusters") - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -192,7 +192,7 @@ talosctl cluster destroy [flags] -n, --nodes strings target the specified nodes --provisioner string Talos cluster provisioner to use (default "docker") --state string directory path to store cluster state (default "/home/user/.talos/clusters") - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -222,7 +222,7 @@ talosctl cluster show [flags] -n, --nodes strings target the specified nodes --provisioner string Talos cluster provisioner to use (default "docker") --state string directory path to store cluster state (default "/home/user/.talos/clusters") - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -248,7 +248,7 @@ A collection of commands for managing local docker-based or QEMU-based clusters --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -321,7 +321,7 @@ talosctl completion SHELL [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -351,7 +351,7 @@ talosctl config add [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -378,7 +378,7 @@ talosctl config context [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -405,7 +405,7 @@ talosctl config contexts [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -432,7 +432,7 @@ talosctl config endpoint ... [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -459,7 +459,7 @@ talosctl config info [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -490,7 +490,7 @@ talosctl config merge [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -519,7 +519,7 @@ talosctl config new [] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -546,7 +546,7 @@ talosctl config node ... [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -569,7 +569,7 @@ Manage the client configuration file (talosconfig) --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -605,7 +605,7 @@ talosctl conformance kubernetes [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -628,7 +628,7 @@ Run conformance tests --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -657,7 +657,7 @@ talosctl containers [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -695,7 +695,7 @@ talosctl copy -| [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -739,7 +739,7 @@ talosctl dashboard [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -767,7 +767,7 @@ talosctl disks [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -796,7 +796,7 @@ talosctl dmesg [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -836,7 +836,7 @@ talosctl edit [] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -863,7 +863,7 @@ talosctl etcd forfeit-leadership [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -890,7 +890,7 @@ talosctl etcd leave [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -917,7 +917,7 @@ talosctl etcd members [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -950,7 +950,7 @@ talosctl etcd remove-member [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -977,7 +977,7 @@ talosctl etcd snapshot [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1000,7 +1000,7 @@ Manage etcd --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1035,7 +1035,7 @@ talosctl events [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1065,7 +1065,7 @@ talosctl gen ca [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1117,7 +1117,7 @@ talosctl gen config [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1148,7 +1148,7 @@ talosctl gen crt [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1178,7 +1178,7 @@ talosctl gen csr [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1206,7 +1206,7 @@ talosctl gen key [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1235,7 +1235,7 @@ talosctl gen keypair [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1266,7 +1266,7 @@ talosctl gen secrets [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1289,7 +1289,7 @@ Generate CAs, certificates, and private keys --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1332,7 +1332,7 @@ talosctl get [] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1366,7 +1366,7 @@ talosctl health [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1393,13 +1393,76 @@ talosctl images [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO * [talosctl](#talosctl) - A CLI for out-of-band management of Kubernetes nodes created by Talos +## talosctl inject serviceaccount + +Inject Talos API ServiceAccount into Kubernetes manifests + +``` +talosctl inject serviceaccount [--roles=','] -f [flags] +``` + +### Examples + +``` +talosctl inject serviceaccount --roles="os:admin" -f deployment.yaml > deployment-injected.yaml + +Alternatively, stdin can be piped to the command: +cat deployment.yaml | talosctl inject serviceaccount --roles="os:admin" -f - > deployment-injected.yaml + +``` + +### Options + +``` + -f, --file string file with Kubernetes manifests to be injected with ServiceAccount + -h, --help help for serviceaccount + -r, --roles strings roles to add to the generated ServiceAccount manifests (default [os:reader]) +``` + +### Options inherited from parent commands + +``` + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + -n, --nodes strings target the specified nodes + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. +``` + +### SEE ALSO + +* [talosctl inject](#talosctl-inject) - Inject Talos API resources into Kubernetes manifests + +## talosctl inject + +Inject Talos API resources into Kubernetes manifests + +### Options + +``` + -h, --help help for inject +``` + +### Options inherited from parent commands + +``` + --context string Context to be used in command + -e, --endpoints strings override default endpoints in Talos configuration + -n, --nodes strings target the specified nodes + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. +``` + +### SEE ALSO + +* [talosctl](#talosctl) - A CLI for out-of-band management of Kubernetes nodes created by Talos +* [talosctl inject serviceaccount](#talosctl-inject-serviceaccount) - Inject Talos API ServiceAccount into Kubernetes manifests + ## talosctl inspect dependencies Inspect controller-resource dependencies as graphviz graph. @@ -1431,7 +1494,7 @@ talosctl inspect dependencies [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1454,7 +1517,7 @@ Inspect internals of Talos --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1491,7 +1554,7 @@ talosctl kubeconfig [local-path] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1526,7 +1589,7 @@ talosctl list [path] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1556,7 +1619,7 @@ talosctl logs [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1584,7 +1647,7 @@ talosctl memory [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1611,7 +1674,7 @@ talosctl mounts [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1644,7 +1707,7 @@ talosctl patch [] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1705,7 +1768,7 @@ talosctl pcap [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1734,7 +1797,7 @@ talosctl processes [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1761,7 +1824,7 @@ talosctl read [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1789,7 +1852,7 @@ talosctl reboot [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1819,7 +1882,7 @@ talosctl reset [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1847,7 +1910,7 @@ talosctl restart [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1874,7 +1937,7 @@ talosctl rollback [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1907,7 +1970,7 @@ talosctl service [ [start|stop|restart|status]] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1935,7 +1998,7 @@ talosctl shutdown [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -1963,7 +2026,7 @@ talosctl stats [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2015,7 +2078,7 @@ talosctl support [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2043,7 +2106,7 @@ talosctl time [--check server] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2074,7 +2137,7 @@ talosctl upgrade [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2110,7 +2173,7 @@ talosctl upgrade-k8s [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2141,7 +2204,7 @@ talosctl usage [path1] [path2] ... [pathN] [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2171,7 +2234,7 @@ talosctl validate [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2201,7 +2264,7 @@ talosctl version [flags] --context string Context to be used in command -e, --endpoints strings override default endpoints in Talos configuration -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2219,7 +2282,7 @@ A CLI for out-of-band management of Kubernetes nodes created by Talos -e, --endpoints strings override default endpoints in Talos configuration -h, --help help for talosctl -n, --nodes strings target the specified nodes - --talosconfig string The path to the Talos configuration file (default "/home/user/.talos/config") + --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. ``` ### SEE ALSO @@ -2242,6 +2305,7 @@ A CLI for out-of-band management of Kubernetes nodes created by Talos * [talosctl get](#talosctl-get) - Get a specific resource or list of resources. * [talosctl health](#talosctl-health) - Check cluster health * [talosctl images](#talosctl-images) - List the default images used by Talos +* [talosctl inject](#talosctl-inject) - Inject Talos API resources into Kubernetes manifests * [talosctl inspect](#talosctl-inspect) - Inspect internals of Talos * [talosctl kubeconfig](#talosctl-kubeconfig) - Download the admin kubeconfig from the node * [talosctl list](#talosctl-list) - Retrieve a directory listing