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