From 05418513ae140ed69c55842634956c650f4ac6ea Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Thu, 30 Nov 2023 15:04:30 -0800 Subject: [PATCH 1/3] Remove deprecated InjectDecoder (#4507) * Remove deprecated InjectDecoder Signed-off-by: Haytham Abuelfutuh * Unit tests Signed-off-by: Haytham Abuelfutuh --------- Signed-off-by: Haytham Abuelfutuh --- flytepropeller/pkg/controller/controller.go | 14 +++++++------- flytepropeller/pkg/webhook/entrypoint.go | 9 ++++----- flytepropeller/pkg/webhook/pod.go | 21 ++++++--------------- flytepropeller/pkg/webhook/pod_test.go | 7 ++----- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/flytepropeller/pkg/controller/controller.go b/flytepropeller/pkg/controller/controller.go index fe9247265f..6b36dc05db 100644 --- a/flytepropeller/pkg/controller/controller.go +++ b/flytepropeller/pkg/controller/controller.go @@ -525,8 +525,7 @@ func SharedInformerOptions(cfg *config.Config, defaultNamespace string) []inform return opts } -func CreateControllerManager(ctx context.Context, cfg *config.Config, options manager.Options) (*manager.Manager, error) { - +func CreateControllerManager(ctx context.Context, cfg *config.Config, options manager.Options) (manager.Manager, error) { _, kubecfg, err := utils.GetKubeConfig(ctx, cfg) if err != nil { return nil, errors.Wrapf(err, "error building Kubernetes Clientset") @@ -536,22 +535,23 @@ func CreateControllerManager(ctx context.Context, cfg *config.Config, options ma if err != nil { return nil, errors.Wrapf(err, "failed to initialize controller-runtime manager") } - return &mgr, nil + + return mgr, nil } // StartControllerManager Start controller runtime manager to start listening to resource changes. // K8sPluginManager uses controller runtime to create informers for the CRDs being monitored by plugins. The informer // EventHandler enqueues the owner workflow for reevaluation. These informer events allow propeller to detect // workflow changes faster than the default sync interval for workflow CRDs. -func StartControllerManager(ctx context.Context, mgr *manager.Manager) error { +func StartControllerManager(ctx context.Context, mgr manager.Manager) error { ctx = contextutils.WithGoroutineLabel(ctx, "controller-runtime-manager") pprof.SetGoroutineLabels(ctx) logger.Infof(ctx, "Starting controller-runtime manager") - return (*mgr).Start(ctx) + return mgr.Start(ctx) } // StartController creates a new FlytePropeller Controller and starts it -func StartController(ctx context.Context, cfg *config.Config, defaultNamespace string, mgr *manager.Manager, scope *promutils.Scope) error { +func StartController(ctx context.Context, cfg *config.Config, defaultNamespace string, mgr manager.Manager, scope *promutils.Scope) error { // Setup cancel on the context ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -591,7 +591,7 @@ func StartController(ctx context.Context, cfg *config.Config, defaultNamespace s informerFactory := k8sInformers.NewSharedInformerFactoryWithOptions(kubeClient, flyteK8sConfig.GetK8sPluginConfig().DefaultPodTemplateResync.Duration) - c, err := New(ctx, cfg, kubeClient, flyteworkflowClient, flyteworkflowInformerFactory, informerFactory, *mgr, *scope) + c, err := New(ctx, cfg, kubeClient, flyteworkflowClient, flyteworkflowInformerFactory, informerFactory, mgr, *scope) if err != nil { return errors.Wrap(err, "failed to start FlytePropeller") } else if c == nil { diff --git a/flytepropeller/pkg/webhook/entrypoint.go b/flytepropeller/pkg/webhook/entrypoint.go index 466cbbde33..ad2c21d6a1 100644 --- a/flytepropeller/pkg/webhook/entrypoint.go +++ b/flytepropeller/pkg/webhook/entrypoint.go @@ -5,11 +5,10 @@ import ( "encoding/json" errors2 "errors" "fmt" - "os" - "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" + "os" "sigs.k8s.io/controller-runtime/pkg/manager" "github.com/flyteorg/flyte/flytepropeller/pkg/controller/config" @@ -25,7 +24,7 @@ const ( ) func Run(ctx context.Context, propellerCfg *config.Config, cfg *config2.Config, - defaultNamespace string, scope *promutils.Scope, mgr *manager.Manager) error { + defaultNamespace string, scope *promutils.Scope, mgr manager.Manager) error { raw, err := json.Marshal(cfg) if err != nil { return err @@ -40,7 +39,7 @@ func Run(ctx context.Context, propellerCfg *config.Config, cfg *config2.Config, webhookScope := (*scope).NewSubScope("webhook") - secretsWebhook := NewPodMutator(cfg, webhookScope) + secretsWebhook := NewPodMutator(cfg, mgr.GetScheme(), webhookScope) // Creates a MutationConfig to instruct ApiServer to call this service whenever a Pod is being created. err = createMutationConfig(ctx, kubeClient, secretsWebhook, defaultNamespace) @@ -48,7 +47,7 @@ func Run(ctx context.Context, propellerCfg *config.Config, cfg *config2.Config, return err } - err = secretsWebhook.Register(ctx, *mgr) + err = secretsWebhook.Register(ctx, mgr) if err != nil { logger.Fatalf(ctx, "Failed to register webhook with manager. Error: %v", err) } diff --git a/flytepropeller/pkg/webhook/pod.go b/flytepropeller/pkg/webhook/pod.go index 556b6053d9..31fca8f9c7 100644 --- a/flytepropeller/pkg/webhook/pod.go +++ b/flytepropeller/pkg/webhook/pod.go @@ -31,6 +31,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "k8s.io/apimachinery/pkg/runtime" "net/http" "os" "path/filepath" @@ -40,7 +41,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -70,17 +70,7 @@ type Mutator interface { Mutate(ctx context.Context, p *corev1.Pod) (newP *corev1.Pod, changed bool, err error) } -func (pm *PodMutator) InjectClient(_ client.Client) error { - return nil -} - -// InjectDecoder injects the decoder into a mutatingHandler. -func (pm *PodMutator) InjectDecoder(d *admission.Decoder) error { - pm.decoder = d - return nil -} - -func (pm *PodMutator) Handle(ctx context.Context, request admission.Request) admission.Response { +func (pm PodMutator) Handle(ctx context.Context, request admission.Request) admission.Response { // Get the object in the request obj := &corev1.Pod{} err := pm.decoder.Decode(request, obj) @@ -132,7 +122,7 @@ func (pm PodMutator) Mutate(ctx context.Context, p *corev1.Pod) (newP *corev1.Po return newP, changed, nil } -func (pm *PodMutator) Register(ctx context.Context, mgr manager.Manager) error { +func (pm PodMutator) Register(ctx context.Context, mgr manager.Manager) error { wh := &admission.Webhook{ Handler: pm, } @@ -220,9 +210,10 @@ func (pm PodMutator) CreateMutationWebhookConfiguration(namespace string) (*admi return mutateConfig, nil } -func NewPodMutator(cfg *config.Config, scope promutils.Scope) *PodMutator { +func NewPodMutator(cfg *config.Config, scheme *runtime.Scheme, scope promutils.Scope) *PodMutator { return &PodMutator{ - cfg: cfg, + decoder: admission.NewDecoder(scheme), + cfg: cfg, Mutators: []MutatorConfig{ { Mutator: NewSecretsMutator(cfg, scope.NewSubScope("secrets")), diff --git a/flytepropeller/pkg/webhook/pod_test.go b/flytepropeller/pkg/webhook/pod_test.go index 122f66c82f..53d8cdee7e 100644 --- a/flytepropeller/pkg/webhook/pod_test.go +++ b/flytepropeller/pkg/webhook/pod_test.go @@ -85,7 +85,7 @@ func Test_CreateMutationWebhookConfiguration(t *testing.T) { pm := NewPodMutator(&config.Config{ CertDir: "testdata", ServiceName: "my-service", - }, promutils.NewTestScope()) + }, latest.Scheme, promutils.NewTestScope()) t.Run("Empty namespace", func(t *testing.T) { c, err := pm.CreateMutationWebhookConfiguration("") @@ -104,10 +104,7 @@ func Test_Handle(t *testing.T) { pm := NewPodMutator(&config.Config{ CertDir: "testdata", ServiceName: "my-service", - }, promutils.NewTestScope()) - - decoder := admission.NewDecoder(latest.Scheme) - assert.NoError(t, pm.InjectDecoder(decoder)) + }, latest.Scheme, promutils.NewTestScope()) req := admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ From 64f097fe0e80cdf44250e04ca861038d5012d845 Mon Sep 17 00:00:00 2001 From: Haytham Abuelfutuh Date: Thu, 30 Nov 2023 15:35:07 -0800 Subject: [PATCH 2/3] Fix /Users/haytham resolution and webhook namespace (#4509) Signed-off-by: Haytham Abuelfutuh Signed-off-by: Eduardo Apolinario <653394+eapolinario@users.noreply.github.com> Co-authored-by: Eduardo Apolinario <653394+eapolinario@users.noreply.github.com> --- cmd/single/start.go | 16 ++++++++++++---- .../pkg/runtime/cluster_resource_provider.go | 3 ++- flytepropeller/cmd/controller/cmd/webhook.go | 2 +- flytepropeller/pkg/webhook/config/config.go | 5 +++++ flytepropeller/pkg/webhook/init_cert.go | 11 ++++++----- flytepropeller/pkg/webhook/pod.go | 16 ++++++++-------- rsts/community/contribute.rst | 12 ++---------- 7 files changed, 36 insertions(+), 29 deletions(-) diff --git a/cmd/single/start.go b/cmd/single/start.go index d6038cac95..3ad8038cd6 100644 --- a/cmd/single/start.go +++ b/cmd/single/start.go @@ -3,6 +3,7 @@ package single import ( "context" "net/http" + "os" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" ctrlWebhook "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -40,6 +41,7 @@ import ( ) const defaultNamespace = "all" +const propellerDefaultNamespace = "flyte" func startDataCatalog(ctx context.Context, _ DataCatalog) error { if err := datacatalogRepo.Migrate(ctx); err != nil { @@ -120,7 +122,7 @@ func startPropeller(ctx context.Context, cfg Propeller) error { SyncPeriod: &propellerCfg.DownstreamEval.Duration, DefaultNamespaces: namespaceConfigs, }, - NewCache: func (config *rest.Config, options cache.Options) (cache.Cache, error) { + NewCache: func(config *rest.Config, options cache.Options) (cache.Cache, error) { k8sCache, err := cache.New(config, options) if err != nil { return k8sCache, err @@ -141,7 +143,7 @@ func startPropeller(ctx context.Context, cfg Propeller) error { BindAddress: "0", }, WebhookServer: ctrlWebhook.NewServer(ctrlWebhook.Options{ - CertDir: webhookConfig.GetConfig().CertDir, + CertDir: webhookConfig.GetConfig().ExpandCertDir(), Port: webhookConfig.GetConfig().ListenPort, }), } @@ -162,7 +164,13 @@ func startPropeller(ctx context.Context, cfg Propeller) error { return err } logger.Infof(childCtx, "Starting Webhook server...") - return webhookEntrypoint.Run(signals.SetupSignalHandler(childCtx), propellerCfg, webhookConfig.GetConfig(), defaultNamespace, &propellerScope, mgr) + // set default namespace for pod template store + podNamespace, found := os.LookupEnv(webhookEntrypoint.PodNamespaceEnvVar) + if !found { + podNamespace = propellerDefaultNamespace + } + + return webhookEntrypoint.Run(signals.SetupSignalHandler(childCtx), propellerCfg, webhookConfig.GetConfig(), podNamespace, &propellerScope, mgr) }) } @@ -207,7 +215,7 @@ var startCmd = &cobra.Command{ for _, serviceName := range []string{otelutils.AdminClientTracer, otelutils.AdminGormTracer, otelutils.AdminServerTracer, otelutils.BlobstoreClientTracer, otelutils.DataCatalogClientTracer, otelutils.DataCatalogGormTracer, otelutils.DataCatalogServerTracer, otelutils.FlytePropellerTracer, otelutils.K8sClientTracer} { - if err := otelutils.RegisterTracerProvider(serviceName, otelutils.GetConfig()) ; err != nil { + if err := otelutils.RegisterTracerProvider(serviceName, otelutils.GetConfig()); err != nil { logger.Errorf(ctx, "Failed to create otel tracer provider. %v", err) return err } diff --git a/flyteadmin/pkg/runtime/cluster_resource_provider.go b/flyteadmin/pkg/runtime/cluster_resource_provider.go index 865d39e9b5..f0ab808c24 100644 --- a/flyteadmin/pkg/runtime/cluster_resource_provider.go +++ b/flyteadmin/pkg/runtime/cluster_resource_provider.go @@ -1,6 +1,7 @@ package runtime import ( + "os" "time" "github.com/flyteorg/flyte/flyteadmin/pkg/runtime/interfaces" @@ -21,7 +22,7 @@ var clusterResourceConfig = config.MustRegisterSection(clusterResourceKey, &inte type ClusterResourceConfigurationProvider struct{} func (p *ClusterResourceConfigurationProvider) GetTemplatePath() string { - return clusterResourceConfig.GetConfig().(*interfaces.ClusterResourceConfig).TemplatePath + return os.ExpandEnv(clusterResourceConfig.GetConfig().(*interfaces.ClusterResourceConfig).TemplatePath) } func (p *ClusterResourceConfigurationProvider) GetTemplateData() interfaces.TemplateData { diff --git a/flytepropeller/cmd/controller/cmd/webhook.go b/flytepropeller/cmd/controller/cmd/webhook.go index eab3851b60..f34f21d12c 100644 --- a/flytepropeller/cmd/controller/cmd/webhook.go +++ b/flytepropeller/cmd/controller/cmd/webhook.go @@ -118,7 +118,7 @@ func runWebhook(origContext context.Context, propellerCfg *config.Config, cfg *w BindAddress: "0", }, WebhookServer: ctrlWebhook.NewServer(ctrlWebhook.Options{ - CertDir: cfg.CertDir, + CertDir: cfg.ExpandCertDir(), Port: cfg.ListenPort, }), } diff --git a/flytepropeller/pkg/webhook/config/config.go b/flytepropeller/pkg/webhook/config/config.go index 4c640bde9e..a1a6fd94ae 100644 --- a/flytepropeller/pkg/webhook/config/config.go +++ b/flytepropeller/pkg/webhook/config/config.go @@ -3,6 +3,7 @@ package config import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + "os" "github.com/flyteorg/flyte/flytestdlib/config" ) @@ -103,6 +104,10 @@ type Config struct { VaultSecretManagerConfig VaultSecretManagerConfig `json:"vaultSecretManager" pflag:",Vault Secret Manager config."` } +func (c Config) ExpandCertDir() string { + return os.ExpandEnv(c.CertDir) +} + type AWSSecretManagerConfig struct { SidecarImage string `json:"sidecarImage" pflag:",Specifies the sidecar docker image to use"` Resources corev1.ResourceRequirements `json:"resources" pflag:"-,Specifies resource requirements for the init container."` diff --git a/flytepropeller/pkg/webhook/init_cert.go b/flytepropeller/pkg/webhook/init_cert.go index 61b86dd66a..e6e08f9054 100644 --- a/flytepropeller/pkg/webhook/init_cert.go +++ b/flytepropeller/pkg/webhook/init_cert.go @@ -78,21 +78,22 @@ func createWebhookSecret(ctx context.Context, namespace string, cfg *webhookConf } if cfg.LocalCert { - if _, err := os.Stat(cfg.CertDir); os.IsNotExist(err) { - if err := os.Mkdir(cfg.CertDir, folderPerm); err != nil { + certPath := cfg.ExpandCertDir() + if _, err := os.Stat(certPath); os.IsNotExist(err) { + if err := os.Mkdir(certPath, folderPerm); err != nil { return err } } - if err := os.WriteFile(path.Join(cfg.CertDir, CaCertKey), certs.CaPEM.Bytes(), permission); err != nil { + if err := os.WriteFile(path.Join(certPath, CaCertKey), certs.CaPEM.Bytes(), permission); err != nil { return err } - if err := os.WriteFile(path.Join(cfg.CertDir, ServerCertKey), certs.ServerPEM.Bytes(), permission); err != nil { + if err := os.WriteFile(path.Join(certPath, ServerCertKey), certs.ServerPEM.Bytes(), permission); err != nil { return err } - if err := os.WriteFile(path.Join(cfg.CertDir, ServerCertPrivateKey), certs.PrivateKeyPEM.Bytes(), permission); err != nil { + if err := os.WriteFile(path.Join(certPath, ServerCertPrivateKey), certs.PrivateKeyPEM.Bytes(), permission); err != nil { return err } } diff --git a/flytepropeller/pkg/webhook/pod.go b/flytepropeller/pkg/webhook/pod.go index 31fca8f9c7..144c9f0a50 100644 --- a/flytepropeller/pkg/webhook/pod.go +++ b/flytepropeller/pkg/webhook/pod.go @@ -1,7 +1,7 @@ -// The PodMutator is a controller-runtime webhook that intercepts Pod Creation events and mutates them. Currently, there -// is only one registered Mutator, that's the SecretsMutator. It works as follows: +// Package webhook container PodMutator. It's a controller-runtime webhook that intercepts Pod Creation events and +// mutates them. Currently, there is only one registered Mutator, that's the SecretsMutator. It works as follows: // -// - The Webhook only works on Pods. If propeller/plugins launch a resource outside of K8s (or in a separate k8s +// - The Webhook only works on Pods. If propeller/plugins launch a resource outside K8s (or in a separate k8s // cluster), it's the responsibility of the plugin to correctly pass secret injection information. // - When a k8s-plugin builds a resource, propeller's PluginManager will automatically inject a label `inject-flyte // -secrets: true` and serialize the secret injection information into the annotations. @@ -9,11 +9,12 @@ // - If a k8s plugin creates a CRD that launches other Pods (e.g. Spark/PyTorch... etc.), it's its responsibility to // make sure the labels/annotations set on the CRD by PluginManager are propagated to those launched Pods. This // ensures secret injection happens no matter how many levels of indirections there are. -// - The Webhook expects 'inject-flyte-secrets: true' as a label on the Pod. Otherwise it won't listen/observe that pod. +// - The Webhook expects 'inject-flyte-secrets: true' as a label on the Pod. Otherwise, it won't listen/observe that +// pod. // - Once it intercepts the admission request, it goes over all registered Mutators and invoke them in the order they -// are registered as. If a Mutator fails and it's marked as `required`, the operation will fail and the admission +// are registered as. If a Mutator fails, and it's marked as `required`, the operation will fail and the admission // will be rejected. -// - The SecretsMutator will attempt to lookup the requested secret from the process environment. If the secret is +// - The SecretsMutator will attempt to look up the requested secret from the process environment. If the secret is // already mounted, it'll inject it as plain-text into the Pod Spec (Less secure). // - If it's not found in the environment it'll, instead, fallback to the enabled Secrets Injector (K8s, Confidant, // Vault... etc.). @@ -30,7 +31,6 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" "k8s.io/apimachinery/pkg/runtime" "net/http" "os" @@ -148,7 +148,7 @@ func generateMutatePath(gvk schema.GroupVersionKind) string { } func (pm PodMutator) CreateMutationWebhookConfiguration(namespace string) (*admissionregistrationv1.MutatingWebhookConfiguration, error) { - caBytes, err := ioutil.ReadFile(filepath.Join(pm.cfg.CertDir, "ca.crt")) + caBytes, err := os.ReadFile(filepath.Join(pm.cfg.ExpandCertDir(), "ca.crt")) if err != nil { // ca.crt is optional. If not provided, API Server will assume the webhook is serving SSL using a certificate // issued by a known Cert Authority. diff --git a/rsts/community/contribute.rst b/rsts/community/contribute.rst index accda13b56..3a92b9e93b 100644 --- a/rsts/community/contribute.rst +++ b/rsts/community/contribute.rst @@ -389,15 +389,7 @@ that integrates all Flyte components into a single binary. go mod tidy make compile - # Step3: Edit the config file: ./flyte-single-binary-local.yaml. - # Replace occurrences of $HOME with the actual path of your home directory. - sedi=(-i) - case "$(uname)" in - Darwin*) sedi=(-i "") - esac - sed "${sedi[@]}" -e "s|\$HOME|${HOME}|g" flyte-single-binary-local.yaml - - # Step 4: Prepare a namespace template for the cluster resource controller. + # Step3: Prepare a namespace template for the cluster resource controller. # The configuration file "flyte-single-binary-local.yaml" has an entry named cluster_resources.templatePath. # This entry needs to direct to a directory containing the templates for the cluster resource controller to use. # We will now create a simple template that allows the automatic creation of required namespaces for projects. @@ -409,7 +401,7 @@ that integrates all Flyte components into a single binary. metadata: name: '{{ namespace }}'" > $HOME/.flyte/cluster-resource-templates/namespace.yaml - # Step5: Running the single binary. + # Step4: Running the single binary. # The POD_NAMESPACE environment variable is necessary for the webhook to function correctly. # You may encounter an error due to `ERROR: duplicate key value violates unique constraint`. Running the command again will solve the problem. POD_NAMESPACE=flyte ./flyte start --config flyte-single-binary-local.yaml From a8f4904353634b34a435402766bdca5d6d7a76b8 Mon Sep 17 00:00:00 2001 From: Jeev B Date: Thu, 30 Nov 2023 16:15:50 -0800 Subject: [PATCH 3/3] Add note on updating sandbox cluster configuration (#4510) * Add note on updating sandbox cluster configuration Signed-off-by: Jeev B * fixes Signed-off-by: Jeev B --------- Signed-off-by: Jeev B --- rsts/deployment/deployment/sandbox.rst | 45 ++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/rsts/deployment/deployment/sandbox.rst b/rsts/deployment/deployment/sandbox.rst index 46809ae4a3..ad99cf7d48 100644 --- a/rsts/deployment/deployment/sandbox.rst +++ b/rsts/deployment/deployment/sandbox.rst @@ -78,12 +78,10 @@ who wish to dig deeper into the storage layer. 🐋 Flyte sandbox ships with a Docker registry. Tag and push custom workflow images to localhost:30000 📂 The Minio API is hosted on localhost:30002. Use http://localhost:30080/minio/login for Minio console -Configuration -______________ +Flytectl/Flyte-remote Configuration +___________________________________ -The ``config-sandbox.yaml`` file contains configuration for **FlyteAdmin**, -which is the Flyte cluster backend component that processes all client requests -such as workflow executions. The default values are enough to let you connect and use Flyte: +The ``config-sandbox.yaml`` file contains configuration for clients to communicate with **FlyteAdmin**, which is the Flyte cluster backend component that processes all client requests such as workflow executions. The default values are enough to let you connect and use Flyte: .. code-block:: yaml @@ -107,6 +105,41 @@ such as workflow executions. The default values are enough to let you connect an Learn more about the configuration settings in the {ref}`Deployment Guide ` +Flyte Cluster Configuration +___________________________ + +Flyte Sandbox ships with a reasonable default configuration. However, you can specify overrides as necessary to fit your use case, in the ``~/.flyte/sandbox/config.yaml`` file. See the following example for enabling the Ray plugin (requires `kuberay-operator `__ to also be installed): + +.. code-block:: shell + + > cat ~/.flyte/sandbox/config.yaml + tasks: + task-plugins: + default-for-task-types: + ray: ray + enabled-plugins: + - container + - sidecar + - k8s-array + - agent-service + - ray + plugins: + ray: + ttlSecondsAfterFinished: 60 + +You can also specify additional cluster resource templates in the ``~/.flyte/sandbox/cluster-resource-templates`` directory. See the following example: + +.. code-block:: shell + + > cat ~/.flyte/sandbox/cluster-resource-templates/001-serviceaccount.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: flyte-worker + namespace: {{ namespace }} + +Once you are happy with the changes, simply run ``flytectl demo reload`` to trigger a reload of the sandbox with the updated configuration. + Now that you have the sandbox cluster running, you can now go to the :ref:`User Guide ` or -:ref:`Tutorials ` to run tasks and workflows written in ``flytekit``, the Python SDK for Flyte. \ No newline at end of file +:ref:`Tutorials ` to run tasks and workflows written in ``flytekit``, the Python SDK for Flyte.