You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by zh...@apache.org on 2025/01/14 12:03:36 UTC

(dubbo-kubernetes) branch master updated: [dubboctl] Update create logic (#540)

This is an automated email from the ASF dual-hosted git repository.

zhongxjian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new d794962a [dubboctl] Update create logic (#540)
d794962a is described below

commit d794962aba7d561d3de4a94fc5e077d4d52032c6
Author: Jian Zhong <11...@qq.com>
AuthorDate: Tue Jan 14 20:03:31 2025 +0800

    [dubboctl] Update create logic (#540)
---
 dubboctl/cmd/create.go                             |  49 ++-
 dubboctl/pkg/filesystem/filesystem.go              | 376 +++++++++++++++++++++
 dubboctl/pkg/sdk/client.go                         | 220 ++----------
 dubboctl/pkg/sdk/dubbo/config.go                   |  63 ++++
 dubboctl/pkg/sdk/template.go                       |  20 ++
 dubboctl/pkg/sdk/templates.go                      |   5 +
 .../pkg/template/go/{common => sample}/.gitignore  |   0
 .../template/go/{common => sample}/api/api.pb.go   |   0
 .../template/go/{common => sample}/api/api.proto   |   0
 .../go/{common => sample}/api/api_triple.pb.go     |   0
 .../pkg/template/go/{common => sample}/cmd/app.go  |   0
 .../go/{common => sample}/conf/dubbogo.yaml        |   0
 dubboctl/pkg/template/go/{common => sample}/go.mod |   0
 dubboctl/pkg/template/go/{common => sample}/go.sum |   0
 .../go/{common => sample}/pkg/service/service.go   |   0
 .../template/java/{common => sample}/.gitignore    |   0
 .../pkg/template/java/{common => sample}/pom.xml   |   0
 .../java/com/example/demo/DemoApplication.java     |   0
 .../example/demo/demos/web/BasicController.java    |   0
 .../demo/demos/web/PathVariableController.java     |   0
 .../main/java/com/example/demo/demos/web/User.java |   0
 .../com/example/demo/dubbo/api/DemoService.java    |   0
 .../com/example/demo/dubbo/consumer/Consumer.java  |   0
 .../demo/dubbo/service/DemoServiceImpl.java        |   0
 .../src/main/resources/application.yml             |   0
 .../src/main/resources/log4j.properties            |   0
 .../src/main/resources/static/index.html           |   0
 .../com/example/demo/DemoApplicationTests.java     |   0
 go.mod                                             |  13 +-
 go.sum                                             |  13 +
 30 files changed, 552 insertions(+), 207 deletions(-)

diff --git a/dubboctl/cmd/create.go b/dubboctl/cmd/create.go
index 32883f06..11e135b7 100644
--- a/dubboctl/cmd/create.go
+++ b/dubboctl/cmd/create.go
@@ -3,9 +3,12 @@ package cmd
 import (
 	"fmt"
 	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/cli"
+	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk"
+	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk/dubbo"
 	"github.com/apache/dubbo-kubernetes/operator/cmd/cluster"
 	"github.com/apache/dubbo-kubernetes/operator/pkg/util/clog"
 	"github.com/apache/dubbo-kubernetes/pkg/kube"
+	"github.com/ory/viper"
 	"github.com/spf13/cobra"
 	"os"
 	"path/filepath"
@@ -75,16 +78,54 @@ func sdkGenerateCmd(ctx cli.Context, _ *cluster.RootArgs, tempArgs *templateArgs
 }
 
 type createArgs struct {
-	dirname string
-	path    string
-	create  string
+	Path       string
+	Runtime    string
+	Template   string
+	Name       string
+	Initialzed bool
 }
 
 func runCreate(kc kube.CLIClient, tempArgs *templateArgs, cl clog.Logger) error {
+	dcfg, err := newCreate(kc, tempArgs, cl)
+	if err != nil {
+		return err
+	}
+	var newClient sdk.ClientFactory
+	var cmd *cobra.Command
+	client, cancel := newClient()
+	defer cancel()
+	_, err = client.Initialize(&dubbo.DubboConfig{
+		Root:     dcfg.Path,
+		Name:     dcfg.Name,
+		Runtime:  dcfg.Runtime,
+		Template: dcfg.Template,
+	}, dcfg.Initialzed, cmd)
+	if err != nil {
+		return err
+	}
+	fmt.Fprintf(cmd.OutOrStderr(), "Created %v dubbo application in %v\n", dcfg.Runtime, dcfg.Path)
 	return nil
 }
 
-func newCreate(kc kube.CLIClient, tempArgs *templateArgs, cl clog.Logger) (createArgs, error) {
+func newCreate(kc kube.CLIClient, tempArgs *templateArgs, cl clog.Logger) (dcfg createArgs, err error) {
+	var (
+		path         string
+		dirName      string
+		absolutePath string
+	)
+	dirName, absolutePath = deriveNameAndAbsolutePathFromPath(path)
+
+	dcfg = createArgs{
+		Path:     absolutePath,
+		Runtime:  viper.GetString("language"),
+		Template: viper.GetString("template"),
+		Name:     dirName,
+	}
+
+	fmt.Printf("Path:         %v\n", dcfg.Path)
+	fmt.Printf("Language:     %v\n", dcfg.Runtime)
+	fmt.Printf("Template:     %v\n", dcfg.Template)
+
 	return createArgs{}, nil
 }
 
diff --git a/dubboctl/pkg/filesystem/filesystem.go b/dubboctl/pkg/filesystem/filesystem.go
new file mode 100644
index 00000000..38172059
--- /dev/null
+++ b/dubboctl/pkg/filesystem/filesystem.go
@@ -0,0 +1,376 @@
+package filesystem
+
+import (
+	"archive/zip"
+	"embed"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
+	"net/url"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+)
+
+import (
+	billy "github.com/go-git/go-billy/v5"
+)
+
+// Filesystems
+// Wrap the implementations of FS with their subtle differences into the
+// common interface for accessing template files defined herein.
+// os:    standard for on-disk extensible template repositories.
+// zip:   embedded filesystem backed by the byte array representing zipfile.
+// billy: go-git library's filesystem used for remote git template repos.
+type Filesystem interface {
+	fs.ReadDirFS
+	fs.StatFS
+	Readlink(link string) (string, error)
+}
+
+type zipFS struct {
+	archive *zip.Reader
+}
+
+func NewZipFS(archive *zip.Reader) zipFS {
+	return zipFS{archive: archive}
+}
+
+func (z zipFS) Open(name string) (fs.File, error) {
+	return z.archive.Open(name)
+}
+
+func (z zipFS) ReadDir(name string) ([]fs.DirEntry, error) {
+	var dirEntries []fs.DirEntry
+	for _, file := range z.archive.File {
+		cleanName := strings.TrimRight(file.Name, "/")
+		if path.Dir(cleanName) == name {
+			f, err := z.archive.Open(cleanName)
+			if err != nil {
+				return nil, err
+			}
+			fi, err := f.Stat()
+			if err != nil {
+				return nil, err
+			}
+			dirEntries = append(dirEntries, dirEntry{FileInfo: fi})
+		}
+	}
+	return dirEntries, nil
+}
+
+func (z zipFS) Readlink(link string) (string, error) {
+	f, err := z.archive.Open(link)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+
+	fi, err := f.Stat()
+	if err != nil {
+		return "", err
+	}
+
+	if fi.Mode()&fs.ModeSymlink == 0 {
+		return "", &fs.PathError{Op: "readlink", Path: link, Err: fs.ErrInvalid}
+	}
+
+	bs, err := io.ReadAll(f)
+	if err != nil {
+		return "", err
+	}
+
+	return string(bs), nil
+}
+
+func (z zipFS) Stat(name string) (fs.FileInfo, error) {
+	f, err := z.archive.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	return f.Stat()
+}
+
+// BillyFilesystem is a template file accessor backed by a billy FS
+type BillyFilesystem struct{ fs billy.Filesystem }
+
+func NewBillyFilesystem(fs billy.Filesystem) BillyFilesystem {
+	return BillyFilesystem{fs: fs}
+}
+
+func (b BillyFilesystem) Readlink(link string) (string, error) {
+	return b.fs.Readlink(link)
+}
+
+type bfsFile struct {
+	billy.File
+	stats func() (fs.FileInfo, error)
+}
+
+func (b bfsFile) Stat() (fs.FileInfo, error) {
+	return b.stats()
+}
+
+func (b BillyFilesystem) Open(name string) (fs.File, error) {
+	f, err := b.fs.Open(name)
+	if err != nil {
+		return nil, err
+	}
+	return bfsFile{
+		File: f,
+		stats: func() (fs.FileInfo, error) {
+			return b.fs.Stat(name)
+		},
+	}, nil
+}
+
+type dirEntry struct {
+	fs.FileInfo
+}
+
+func (d dirEntry) Type() fs.FileMode {
+	return d.Mode().Type()
+}
+
+func (d dirEntry) Info() (fs.FileInfo, error) {
+	return d, nil
+}
+
+func (b BillyFilesystem) ReadDir(name string) ([]fs.DirEntry, error) {
+	fis, err := b.fs.ReadDir(name)
+	if err != nil {
+		return nil, err
+	}
+	des := make([]fs.DirEntry, len(fis))
+	for i, fi := range fis {
+		des[i] = dirEntry{fi}
+	}
+	return des, nil
+}
+
+func (b BillyFilesystem) Stat(name string) (fs.FileInfo, error) {
+	return b.fs.Lstat(name)
+}
+
+// osFilesystem is a template file accessor backed by the os.
+type osFilesystem struct{ root string }
+
+func NewOsFilesystem(root string) osFilesystem {
+	return osFilesystem{root: root}
+}
+
+func (o osFilesystem) Open(name string) (fs.File, error) {
+	name = filepath.FromSlash(name)
+	return os.Open(filepath.Join(o.root, name))
+}
+
+func (o osFilesystem) ReadDir(name string) ([]fs.DirEntry, error) {
+	name = filepath.FromSlash(name)
+	return os.ReadDir(filepath.Join(o.root, name))
+}
+
+func (o osFilesystem) Stat(name string) (fs.FileInfo, error) {
+	name = filepath.FromSlash(name)
+	return os.Lstat(filepath.Join(o.root, name))
+}
+
+func (o osFilesystem) Readlink(link string) (string, error) {
+	link = filepath.FromSlash(link)
+	return os.Readlink(filepath.Join(o.root, link))
+}
+
+// subFS exposes subdirectory of underlying FS, this is similar to `chroot`.
+type subFS struct {
+	root string
+	fs   Filesystem
+}
+
+func NewSubFS(root string, fs Filesystem) subFS {
+	return subFS{root: root, fs: fs}
+}
+
+func (o subFS) Open(name string) (fs.File, error) {
+	return o.fs.Open(path.Join(o.root, name))
+}
+
+func (o subFS) ReadDir(name string) ([]fs.DirEntry, error) {
+	return o.fs.ReadDir(path.Join(o.root, name))
+}
+
+func (o subFS) Stat(name string) (fs.FileInfo, error) {
+	return o.fs.Stat(path.Join(o.root, name))
+}
+
+func (o subFS) Readlink(link string) (string, error) {
+	return o.fs.Readlink(path.Join(o.root, link))
+}
+
+type maskingFS struct {
+	masked func(path string) bool
+	fs     Filesystem
+}
+
+func NewMaskingFS(masked func(path string) bool, fs Filesystem) maskingFS {
+	return maskingFS{masked: masked, fs: fs}
+}
+
+func (m maskingFS) Open(name string) (fs.File, error) {
+	if m.masked(name) {
+		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
+	}
+	return m.fs.Open(name)
+}
+
+func (m maskingFS) ReadDir(name string) ([]fs.DirEntry, error) {
+	if m.masked(name) {
+		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
+	}
+	des, err := m.fs.ReadDir(name)
+	if err != nil {
+		return nil, err
+	}
+	result := make([]fs.DirEntry, 0, len(des))
+	for _, de := range des {
+		if !m.masked(path.Join(name, de.Name())) {
+			result = append(result, de)
+		}
+	}
+	return result, nil
+}
+
+func (m maskingFS) Stat(name string) (fs.FileInfo, error) {
+	if m.masked(name) {
+		return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist}
+	}
+	return m.fs.Stat(name)
+}
+
+func (m maskingFS) Readlink(link string) (string, error) {
+	if m.masked(link) {
+		return "", &fs.PathError{Op: "readlink", Path: link, Err: fs.ErrNotExist}
+	}
+	return m.fs.Readlink(link)
+}
+
+const (
+	EmbedSchema = "embed"
+)
+
+// UnionFS is an os.FS and embed.FS union fs.
+// Files in embed.FS has the header "embed://", and files in os.FS don't have this header.
+type UnionFS struct {
+	embedFS embed.FS
+}
+
+func NewUnionFS(embedFS embed.FS) UnionFS {
+	return UnionFS{
+		embedFS: embedFS,
+	}
+}
+
+func (u UnionFS) ReadFile(name string) ([]byte, error) {
+	uri, _ := url.Parse(name)
+	if uri != nil && uri.Scheme == EmbedSchema {
+		name = strings.TrimLeft(uri.Opaque, "/")
+		return u.embedFS.ReadFile(name)
+	}
+
+	return os.ReadFile(name)
+}
+
+func (u UnionFS) Open(name string) (fs.File, error) {
+	uri, _ := url.Parse(name)
+	if uri != nil && uri.Scheme == EmbedSchema {
+		name = strings.TrimLeft(uri.Opaque, "/")
+		return u.embedFS.Open(name)
+	}
+
+	return os.Open(name)
+}
+
+func (u UnionFS) ReadDir(name string) ([]fs.DirEntry, error) {
+	uri, _ := url.Parse(name)
+	if uri != nil && uri.Scheme == EmbedSchema {
+		name = strings.TrimLeft(uri.Opaque, "/")
+		return u.embedFS.ReadDir(name)
+	}
+
+	return os.ReadDir(name)
+}
+
+func (u UnionFS) Stat(name string) (fs.FileInfo, error) {
+	f, err := u.Open(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return f.Stat()
+}
+
+func (u UnionFS) Readlink(link string) (string, error) {
+	uri, _ := url.Parse(link)
+	if uri != nil && uri.Scheme == EmbedSchema {
+		return "", errors.New("embed FS not support read link")
+	}
+
+	return os.Readlink(link)
+}
+
+// CopyFromFS copies files from the `src` dir on the accessor Filesystem to local filesystem into `dest` dir.
+// The src path uses slashes as their separator.
+// The dest path uses OS specific separator.
+func CopyFromFS(root, dest string, fsys Filesystem) (err error) {
+	// Walks the filesystem rooted at root.
+	return fs.WalkDir(fsys, root, func(path string, de fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		p, err := filepath.Rel(filepath.FromSlash(root), filepath.FromSlash(path))
+		if err != nil {
+			return err
+		}
+
+		dest := filepath.Join(dest, p)
+
+		switch {
+		case de.IsDir():
+			// Ideally we should use the file mode of the src node
+			// but it seems the git module is reporting directories
+			// as 0644 instead of 0755. For now, just do it this way.
+			// See https://github.com/go-git/go-git/issues/364
+			// Upon resolution, return accessor.Stat(src).Mode()
+			return os.MkdirAll(dest, 0o755)
+		case de.Type()&fs.ModeSymlink != 0:
+			var symlinkTarget string
+			symlinkTarget, err = fsys.Readlink(path)
+			if err != nil {
+				return err
+			}
+			return os.Symlink(symlinkTarget, dest)
+		case de.Type().IsRegular():
+			fi, err := de.Info()
+			if err != nil {
+				return err
+			}
+			destFile, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fi.Mode())
+			if err != nil {
+				return err
+			}
+			defer destFile.Close()
+
+			srcFile, err := fsys.Open(path)
+			if err != nil {
+				return err
+			}
+			defer srcFile.Close()
+
+			_, err = io.Copy(destFile, srcFile)
+			return err
+		default:
+			return fmt.Errorf("unsuported file type: %s", de.Type().String())
+		}
+	})
+}
diff --git a/dubboctl/pkg/sdk/client.go b/dubboctl/pkg/sdk/client.go
index ead010a6..f2968576 100644
--- a/dubboctl/pkg/sdk/client.go
+++ b/dubboctl/pkg/sdk/client.go
@@ -2,132 +2,38 @@ package sdk
 
 import (
 	"bufio"
-	"context"
 	"fmt"
+	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk/dubbo"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
-	"gopkg.in/yaml.v3"
 	"os"
 	"path/filepath"
+	"sigs.k8s.io/yaml"
 	"strings"
 	"time"
 )
 
-const (
-	DubboFile       = "dubbo.yaml"
-	RunDataDir      = ".dubbo"
-	DefaultTemplate = "common"
-)
-
 type Client struct {
 	templates *Templates
 }
-type Option func(*Client)
-
-func New(options ...Option) *Client {
-	c := &Client{}
-	for _, o := range options {
-		o(c)
-	}
-
-	c.templates = newTemplates(c)
-
-	return c
-}
-
-func newTemplates(client *Client) *Templates {
-	return &Templates{client: client}
-}
-
-type Templates struct {
-	client *Client
-}
-
-type DubboConfig struct {
-	Root     string    `yaml:"-"`
-	Name     string    `yaml:"name,omitempty" jsonschema:"pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"`
-	Runtime  string    `yaml:"runtime,omitempty"`
-	Template string    `yaml:"template,omitempty"`
-	Created  time.Time `yaml:"created,omitempty"`
-}
-
-func (f *DubboConfig) Validate() error {
-	if f.Root == "" {
-		return errors.New("dubbo root path is required")
-	}
-
-	var ctr int
-	errs := [][]string{
-		validateOptions(),
-	}
-
-	var b strings.Builder
-	b.WriteString(fmt.Sprintf("'%v' contains errors:", DubboFile))
-
-	for _, ee := range errs {
-		if len(ee) > 0 {
-			b.WriteString("\n") // Precede each group of errors with a linebreak
-		}
-		for _, e := range ee {
-			ctr++
-			b.WriteString("\t" + e)
-		}
-	}
 
-	if ctr == 0 {
-		return nil // Return nil if there were no validation errors.
-	}
+type Option func(*Client)
 
-	return errors.New(b.String())
-}
-
-func validateOptions() []string {
-	return nil
-}
+type ClientFactory func(...Option) (*Client, func())
 
 func (c *Client) Templates() *Templates {
 	return c.templates
 }
 
-func (f *DubboConfig) Write() (err error) {
-	if err = f.Validate(); err != nil {
-		return
-	}
-	dubboyamlpath := filepath.Join(f.Root, DubboFile)
-	var dubbobytes []byte
-	if dubbobytes, err = yaml.Marshal(f); err != nil {
-		return
-	}
-	if err = os.WriteFile(dubboyamlpath, dubbobytes, 0644); err != nil {
-		return
-	}
-	return
-}
-
-type Template interface {
-	Name() string
-	Runtime() string
-	Repository() string
-	Fullname() string
-	Write(ctx context.Context, f *DubboConfig) error
-}
-
-type template struct {
-	name    string
-	runtime string
-}
-
-func (t template) Name() string {
-	return t.name
+func (c *Client) Runtimes() ([]string, error) {
+	// TODO
+	return nil, nil
 }
 
-func (t template) Runtime() string {
-	return t.runtime
-}
-
-func (c *Client) Create(dcfg *DubboConfig, initial bool, cmd *cobra.Command) (*DubboConfig, error) {
+func (c *Client) Initialize(dcfg *dubbo.DubboConfig, initialized bool, _ *cobra.Command) (*dubbo.DubboConfig, error) {
 	var err error
 	oldRoot := dcfg.Root
+
 	dcfg.Root, err = filepath.Abs(dcfg.Root)
 	if err != nil {
 		return dcfg, err
@@ -137,12 +43,12 @@ func (c *Client) Create(dcfg *DubboConfig, initial bool, cmd *cobra.Command) (*D
 		return dcfg, err
 	}
 
-	has, err := hasInitializedApplication(dcfg.Root)
+	has, err := hasInitialized(dcfg.Root)
 	if err != nil {
 		return dcfg, err
 	}
 	if has {
-		return dcfg, fmt.Errorf("application at '%v' already initialized", dcfg.Root)
+		return dcfg, fmt.Errorf("%v already initialized", dcfg.Root)
 	}
 
 	if dcfg.Root == "" {
@@ -150,90 +56,40 @@ func (c *Client) Create(dcfg *DubboConfig, initial bool, cmd *cobra.Command) (*D
 			return dcfg, err
 		}
 	}
-
 	if dcfg.Name == "" {
 		dcfg.Name = nameFromPath(dcfg.Root)
 	}
 
-	if !initial {
+	if !initialized {
 		if err := assertEmptyRoot(dcfg.Root); err != nil {
 			return dcfg, err
 		}
 	}
 
-	f := NewDubboWithTemplate(dcfg, initial)
-
+	f := dubbo.NewDubboConfigWithTemplate(dcfg, initialized)
 	if err = EnsureRunDataDir(f.Root); err != nil {
 		return f, err
 	}
 
 	f.Created = time.Now()
-	err = f.Write()
-	if err != nil {
-		return f, err
-	}
-
-	return NewDubbo(oldRoot)
-}
-
-func NewDubbo(path string) (*DubboConfig, error) {
-	var err error
-	f := &DubboConfig{}
-	// Path defaults to current working directory, and if provided explicitly
-	// Path must exist and be a directory
-	if path == "" {
-		if path, err = os.Getwd(); err != nil {
-			return f, err
-		}
-	}
-	f.Root = path // path is not persisted, as this is the purview of the FS
-
-	// Path must exist and be a directory
-	fd, err := os.Stat(path)
-	if err != nil {
-		return f, err
-	}
-	if !fd.IsDir() {
-		return f, fmt.Errorf("function path must be a directory")
-	}
-
-	// If no dubbo.yaml in directory, return the default function which will
-	// have f.Initialized() == false
-	filename := filepath.Join(path, DubboFile)
-	if _, err = os.Stat(filename); err != nil {
-		if os.IsNotExist(err) {
-			err = nil
-		}
-		return f, err
-	}
-
-	// Path is valid and dubbo.yaml exists: load it
-	bb, err := os.ReadFile(filename)
-	if err != nil {
-		return f, err
-	}
-	err = yaml.Unmarshal(bb, f)
-	if err != nil {
-		return f, err
-	}
-	return f, nil
+	return dubbo.NewDubboConfig(oldRoot)
 }
 
-func hasInitializedApplication(path string) (bool, error) {
+func hasInitialized(path string) (bool, error) {
 	var err error
-	filename := filepath.Join(path, DubboFile)
+	filename := filepath.Join(path, dubbo.DubboFile)
 
 	if _, err = os.Stat(filename); err != nil {
 		if os.IsNotExist(err) {
 			return false, nil
 		}
-		return false, err
+		return false, err // invalid path or access error
 	}
 	bb, err := os.ReadFile(filename)
 	if err != nil {
 		return false, err
 	}
-	f := DubboConfig{}
+	f := dubbo.DubboConfig{}
 	if err = yaml.Unmarshal(bb, &f); err != nil {
 		return false, err
 	}
@@ -241,10 +97,6 @@ func hasInitializedApplication(path string) (bool, error) {
 	return f.Initialized(), nil
 }
 
-func (f *DubboConfig) Initialized() bool {
-	return !f.Created.IsZero()
-}
-
 func nameFromPath(path string) string {
 	pathParts := strings.Split(strings.TrimRight(path, string(os.PathSeparator)), string(os.PathSeparator))
 	return pathParts[len(pathParts)-1]
@@ -261,15 +113,12 @@ func nameFromPath(path string) string {
 }
 
 func assertEmptyRoot(path string) (err error) {
-	// If there exists contentious files (config files for instance), this function may have already been initialized.
 	files, err := contentiousFilesIn(path)
 	if err != nil {
 		return
 	} else if len(files) > 0 {
 		return fmt.Errorf("the chosen directory '%v' contains contentious files: %v.  Has the Service function already been created?  Try either using a different directory, deleting the function if it exists, or manually removing the files", path, files)
 	}
-
-	// Ensure there are no non-hidden files, and again none of the aforementioned contentious files.
 	empty, err := isEffectivelyEmpty(path)
 	if err != nil {
 		return
@@ -281,11 +130,10 @@ func assertEmptyRoot(path string) (err error) {
 }
 
 var contentiousFiles = []string{
-	DubboFile,
+	dubbo.DubboFile,
 	".gitignore",
 }
 
-// contentiousFilesIn the given directory
 func contentiousFilesIn(dir string) (contentious []string, err error) {
 	files, err := os.ReadDir(dir)
 	for _, file := range files {
@@ -313,18 +161,9 @@ func isEffectivelyEmpty(dir string) (bool, error) {
 }
 
 func EnsureRunDataDir(root string) error {
-	// Ensure the runtime directory exists
-	if err := os.MkdirAll(filepath.Join(root, RunDataDir), os.ModePerm); err != nil {
+	if err := os.MkdirAll(filepath.Join(root, dubbo.DataDir), os.ModePerm); err != nil {
 		return err
 	}
-
-	// Update .gitignore
-	//
-	// Ensure .dubbo is added to .gitignore unless the user explicitly
-	// commented out the ignore line for some awful reason.
-	// Also creates the .gitignore in the application's root directory if it does
-	// not already exist (note that this may not be in the root of the repo
-	// if the function is at a subpart of a monorepo)
 	filePath := filepath.Join(root, ".gitignore")
 	roFile, err := os.Open(filePath)
 	if err != nil && !os.IsNotExist(err) {
@@ -334,19 +173,17 @@ func EnsureRunDataDir(root string) error {
 	if !os.IsNotExist(err) { // if no error openeing it
 		s := bufio.NewScanner(roFile) // create a scanner
 		for s.Scan() {                // scan each line
-			if strings.HasPrefix(s.Text(), "# /"+RunDataDir) { // if it was commented
+			if strings.HasPrefix(s.Text(), "# /"+dubbo.DataDir) { // if it was commented
 				return nil // user wants it
 			}
-			if strings.HasPrefix(s.Text(), "#/"+RunDataDir) {
+			if strings.HasPrefix(s.Text(), "#/"+dubbo.DataDir) {
 				return nil // user wants it
 			}
-			if strings.HasPrefix(s.Text(), "/"+RunDataDir) { // if it is there
+			if strings.HasPrefix(s.Text(), "/"+dubbo.DataDir) { // if it is there
 				return nil // we're done
 			}
 		}
 	}
-	// Either .gitignore does not exist or it does not have the ignore
-	// directive for .func yet.
 	roFile.Close()
 	rwFile, err := os.OpenFile(filePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644)
 	if err != nil {
@@ -369,14 +206,3 @@ func EnsureRunDataDir(root string) error {
 	}
 	return nil
 }
-
-func NewDubboWithTemplate(defaults *DubboConfig, init bool) *DubboConfig {
-	if !init {
-		if defaults.Template == "" {
-			defaults.Template = DefaultTemplate
-		}
-	} else {
-		defaults.Template = "init"
-	}
-	return defaults
-}
diff --git a/dubboctl/pkg/sdk/dubbo/config.go b/dubboctl/pkg/sdk/dubbo/config.go
new file mode 100644
index 00000000..9ce9dd45
--- /dev/null
+++ b/dubboctl/pkg/sdk/dubbo/config.go
@@ -0,0 +1,63 @@
+package dubbo
+
+import (
+	"gopkg.in/yaml.v3"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+const (
+	DubboFile = "dubbo.yaml"
+	DataDir   = ".dubbo"
+)
+
+type DubboConfig struct {
+	Root     string    `yaml:"-"`
+	Name     string    `yaml:"name,omitempty" jsonschema:"pattern=^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"`
+	Runtime  string    `yaml:"runtime,omitempty"`
+	Template string    `yaml:"template,omitempty"`
+	Created  time.Time `yaml:"created,omitempty"`
+}
+
+func NewDubboConfig(path string) (*DubboConfig, error) {
+	var err error
+
+	fd, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	if !fd.IsDir() {
+		return nil, nil
+	}
+	filename := filepath.Join(path, DubboFile)
+	if _, err = os.Stat(filename); err != nil {
+		if os.IsNotExist(err) {
+			err = nil
+		}
+		return nil, err
+	}
+
+	bb, err := os.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	err = yaml.Unmarshal(bb, nil)
+	if err != nil {
+		return nil, err
+	}
+	return nil, nil
+}
+
+func NewDubboConfigWithTemplate(defaults *DubboConfig, initialized bool) *DubboConfig {
+	if !initialized {
+		if defaults.Template == "" {
+			defaults.Template = "sample"
+		}
+	}
+	return defaults
+}
+
+func (dc *DubboConfig) Initialized() bool {
+	return !dc.Created.IsZero()
+}
diff --git a/dubboctl/pkg/sdk/template.go b/dubboctl/pkg/sdk/template.go
new file mode 100644
index 00000000..7ca369c4
--- /dev/null
+++ b/dubboctl/pkg/sdk/template.go
@@ -0,0 +1,20 @@
+package sdk
+
+import (
+	"context"
+	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/filesystem"
+	"github.com/apache/dubbo-kubernetes/dubboctl/pkg/sdk/dubbo"
+)
+
+type template struct {
+	name    string
+	runtime string
+	fs      filesystem.Filesystem
+}
+
+type Template interface {
+	Name() string
+	Fullname() string
+	Runtime() string
+	Write(ctx context.Context, f *dubbo.DubboConfig) error
+}
diff --git a/dubboctl/pkg/sdk/templates.go b/dubboctl/pkg/sdk/templates.go
new file mode 100644
index 00000000..9c0759ac
--- /dev/null
+++ b/dubboctl/pkg/sdk/templates.go
@@ -0,0 +1,5 @@
+package sdk
+
+type Templates struct {
+	client *Client
+}
diff --git a/dubboctl/pkg/template/go/common/.gitignore b/dubboctl/pkg/template/go/sample/.gitignore
similarity index 100%
rename from dubboctl/pkg/template/go/common/.gitignore
rename to dubboctl/pkg/template/go/sample/.gitignore
diff --git a/dubboctl/pkg/template/go/common/api/api.pb.go b/dubboctl/pkg/template/go/sample/api/api.pb.go
similarity index 100%
rename from dubboctl/pkg/template/go/common/api/api.pb.go
rename to dubboctl/pkg/template/go/sample/api/api.pb.go
diff --git a/dubboctl/pkg/template/go/common/api/api.proto b/dubboctl/pkg/template/go/sample/api/api.proto
similarity index 100%
rename from dubboctl/pkg/template/go/common/api/api.proto
rename to dubboctl/pkg/template/go/sample/api/api.proto
diff --git a/dubboctl/pkg/template/go/common/api/api_triple.pb.go b/dubboctl/pkg/template/go/sample/api/api_triple.pb.go
similarity index 100%
rename from dubboctl/pkg/template/go/common/api/api_triple.pb.go
rename to dubboctl/pkg/template/go/sample/api/api_triple.pb.go
diff --git a/dubboctl/pkg/template/go/common/cmd/app.go b/dubboctl/pkg/template/go/sample/cmd/app.go
similarity index 100%
rename from dubboctl/pkg/template/go/common/cmd/app.go
rename to dubboctl/pkg/template/go/sample/cmd/app.go
diff --git a/dubboctl/pkg/template/go/common/conf/dubbogo.yaml b/dubboctl/pkg/template/go/sample/conf/dubbogo.yaml
similarity index 100%
rename from dubboctl/pkg/template/go/common/conf/dubbogo.yaml
rename to dubboctl/pkg/template/go/sample/conf/dubbogo.yaml
diff --git a/dubboctl/pkg/template/go/common/go.mod b/dubboctl/pkg/template/go/sample/go.mod
similarity index 100%
rename from dubboctl/pkg/template/go/common/go.mod
rename to dubboctl/pkg/template/go/sample/go.mod
diff --git a/dubboctl/pkg/template/go/common/go.sum b/dubboctl/pkg/template/go/sample/go.sum
similarity index 100%
rename from dubboctl/pkg/template/go/common/go.sum
rename to dubboctl/pkg/template/go/sample/go.sum
diff --git a/dubboctl/pkg/template/go/common/pkg/service/service.go b/dubboctl/pkg/template/go/sample/pkg/service/service.go
similarity index 100%
rename from dubboctl/pkg/template/go/common/pkg/service/service.go
rename to dubboctl/pkg/template/go/sample/pkg/service/service.go
diff --git a/dubboctl/pkg/template/java/common/.gitignore b/dubboctl/pkg/template/java/sample/.gitignore
similarity index 100%
rename from dubboctl/pkg/template/java/common/.gitignore
rename to dubboctl/pkg/template/java/sample/.gitignore
diff --git a/dubboctl/pkg/template/java/common/pom.xml b/dubboctl/pkg/template/java/sample/pom.xml
similarity index 100%
rename from dubboctl/pkg/template/java/common/pom.xml
rename to dubboctl/pkg/template/java/sample/pom.xml
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/DemoApplication.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/DemoApplication.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/DemoApplication.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/DemoApplication.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/BasicController.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/BasicController.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/BasicController.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/BasicController.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/PathVariableController.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/PathVariableController.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/PathVariableController.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/PathVariableController.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/User.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/User.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/demos/web/User.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/demos/web/User.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/api/DemoService.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/api/DemoService.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/api/DemoService.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/api/DemoService.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/consumer/Consumer.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/consumer/Consumer.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/consumer/Consumer.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/consumer/Consumer.java
diff --git a/dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/service/DemoServiceImpl.java b/dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/service/DemoServiceImpl.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/java/com/example/demo/dubbo/service/DemoServiceImpl.java
rename to dubboctl/pkg/template/java/sample/src/main/java/com/example/demo/dubbo/service/DemoServiceImpl.java
diff --git a/dubboctl/pkg/template/java/common/src/main/resources/application.yml b/dubboctl/pkg/template/java/sample/src/main/resources/application.yml
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/resources/application.yml
rename to dubboctl/pkg/template/java/sample/src/main/resources/application.yml
diff --git a/dubboctl/pkg/template/java/common/src/main/resources/log4j.properties b/dubboctl/pkg/template/java/sample/src/main/resources/log4j.properties
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/resources/log4j.properties
rename to dubboctl/pkg/template/java/sample/src/main/resources/log4j.properties
diff --git a/dubboctl/pkg/template/java/common/src/main/resources/static/index.html b/dubboctl/pkg/template/java/sample/src/main/resources/static/index.html
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/main/resources/static/index.html
rename to dubboctl/pkg/template/java/sample/src/main/resources/static/index.html
diff --git a/dubboctl/pkg/template/java/common/src/test/java/com/example/demo/DemoApplicationTests.java b/dubboctl/pkg/template/java/sample/src/test/java/com/example/demo/DemoApplicationTests.java
similarity index 100%
rename from dubboctl/pkg/template/java/common/src/test/java/com/example/demo/DemoApplicationTests.java
rename to dubboctl/pkg/template/java/sample/src/test/java/com/example/demo/DemoApplicationTests.java
diff --git a/go.mod b/go.mod
index 5473dfe5..14eb4d83 100644
--- a/go.mod
+++ b/go.mod
@@ -58,15 +58,15 @@ require (
 	github.com/spf13/cobra v1.8.1
 	github.com/spf13/pflag v1.0.5
 	github.com/spiffe/go-spiffe/v2 v2.1.7
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.10.0
 	go.opentelemetry.io/otel/trace v1.28.0
 	go.uber.org/atomic v1.10.0
 	go.uber.org/multierr v1.11.0
 	go.uber.org/zap v1.27.0
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
-	golang.org/x/net v0.33.0
+	golang.org/x/net v0.34.0
 	golang.org/x/sync v0.10.0
-	golang.org/x/sys v0.28.0
+	golang.org/x/sys v0.29.0
 	golang.org/x/text v0.21.0
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7
 	google.golang.org/grpc v1.65.0
@@ -120,7 +120,7 @@ require (
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/creasty/defaults v1.5.2 // indirect
-	github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+	github.com/cyphar/filepath-securejoin v0.3.6 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dgraph-io/ristretto v0.0.1 // indirect
 	github.com/dlclark/regexp2 v1.7.0 // indirect
@@ -135,6 +135,7 @@ require (
 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-errors/errors v1.4.2 // indirect
+	github.com/go-git/go-billy/v5 v5.6.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
@@ -245,10 +246,10 @@ require (
 	go.opentelemetry.io/otel/sdk v1.28.0 // indirect
 	go.opentelemetry.io/proto/otlp v1.3.1 // indirect
 	golang.org/x/arch v0.3.0 // indirect
-	golang.org/x/crypto v0.31.0 // indirect
+	golang.org/x/crypto v0.32.0 // indirect
 	golang.org/x/mod v0.22.0 // indirect
 	golang.org/x/oauth2 v0.23.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
+	golang.org/x/term v0.28.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	golang.org/x/tools v0.28.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 666216d4..abe27870 100644
--- a/go.sum
+++ b/go.sum
@@ -201,6 +201,8 @@ github.com/creasty/defaults v1.5.2 h1:/VfB6uxpyp6h0fr7SPp7n8WJBoV8jfxQXPCnkVSjyl
 github.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
 github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
 github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
+github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
+github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -302,6 +304,8 @@ github.com/go-co-op/gocron v1.9.0/go.mod h1:DbJm9kdgr1sEvWpHCA7dFFs/PGHPMil9/97E
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
+github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -950,6 +954,7 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
@@ -1117,6 +1122,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1212,6 +1219,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1321,11 +1330,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=