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 9a2a13cf [dubboctl] Recovering from missing git records
9a2a13cf is described below
commit 9a2a13cfd528bc85d02214486bf6f1ca220aa34d
Author: mfordjody <[email protected]>
AuthorDate: Tue Oct 29 21:01:19 2024 +0800
[dubboctl] Recovering from missing git records
---
dubboctl/cmd/build.go | 220 +++++++++++++++
dubboctl/cmd/client.go | 87 ++++++
dubboctl/cmd/common.go | 26 ++
dubboctl/cmd/completion_util.go | 73 +++++
dubboctl/cmd/create.go | 572 ++++++++++++++++++++++++++++++++++++++
dubboctl/cmd/deploy.go | 317 +++++++++++++++++++++
dubboctl/cmd/repository.go | 596 ++++++++++++++++++++++++++++++++++++++++
7 files changed, 1891 insertions(+)
diff --git a/dubboctl/cmd/build.go b/dubboctl/cmd/build.go
new file mode 100644
index 00000000..9d6278d5
--- /dev/null
+++ b/dubboctl/cmd/build.go
@@ -0,0 +1,220 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "strings"
+)
+
+import (
+ "github.com/AlecAivazis/survey/v2"
+
+ "github.com/ory/viper"
+
+ "github.com/spf13/cobra"
+)
+
+import (
+
"github.com/apache/dubbo-kubernetes/dubboctl/internal/builders/dockerfile"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/builders/pack"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/util"
+)
+
+func addBuild(baseCmd *cobra.Command, newClient ClientFactory) {
+ cmd := &cobra.Command{
+ Use: "build",
+ Short: "Build the image for the application",
+ Long: ``,
+ SuggestFor: []string{"biuld", "buidl", "built"},
+ PreRunE: bindEnv("useDockerfile", "image", "path", "push",
"force", "envs",
+ "builder-image"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runBuildCmd(cmd, newClient)
+ },
+ }
+
+ cmd.Flags().StringP("builder-image", "b", "",
+ "Specify a custom builder image for use by the builder other
than its default.")
+ cmd.Flags().BoolP("useDockerfile", "d", false,
+ "Use the dockerfile with the specified path to build")
+ cmd.Flags().StringP("image", "i", "",
+ "Container image( [registry]/[namespace]/[name]:[tag] )")
+ cmd.Flags().BoolP("push", "", true,
+ "Whether to push the image to the registry center by the way")
+ cmd.Flags().BoolP("force", "f", false,
+ "Whether to force build")
+ cmd.Flags().StringArrayP("envs", "e", []string{},
+ "environment variable for an application, KEY=VALUE format")
+ addPathFlag(cmd)
+ baseCmd.AddCommand(cmd)
+}
+
+func runBuildCmd(cmd *cobra.Command, newClient ClientFactory) error {
+ if err := util.CreatePaths(); err != nil {
+ return err
+ }
+ cfg := newBuildConfig(cmd)
+ f, err := dubbo.NewDubbo(cfg.Path)
+ if err != nil {
+ return err
+ }
+
+ cfg, err = cfg.Prompt(f)
+ if err != nil {
+ return err
+ }
+ if !f.Initialized() {
+ return dubbo.NewErrNotInitialized(f.Root)
+ }
+ cfg.Configure(f)
+
+ clientOptions, err := cfg.buildclientOptions()
+ if err != nil {
+ return err
+ }
+ client, done := newClient(clientOptions...)
+ defer done()
+ if f.Built() && !cfg.Force {
+ fmt.Fprintln(cmd.OutOrStdout(), "The Application is up to date,
If you still want to build, use `--force true`")
+ return nil
+ }
+ if f, err = client.Build(cmd.Context(), f); err != nil {
+ return err
+ }
+ if cfg.Push {
+ if f, err = client.Push(cmd.Context(), f); err != nil {
+ return err
+ }
+ }
+
+ if err = f.Write(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *buildConfig) Prompt(d *dubbo.Dubbo) (*buildConfig, error) {
+ var err error
+ if !util.InteractiveTerminal() {
+ return c, nil
+ }
+
+ if c.Image == "" && d.Image == "" {
+
+ qs := []*survey.Question{
+ {
+ Name: "image",
+ Validate: survey.Required,
+ Prompt: &survey.Input{
+ Message: "the container image(
[registry]/[namespace]/[name]:[tag] ). For example:
docker.io/sjmshsh/testapp:latest",
+ Default: c.Image,
+ },
+ },
+ }
+ if err = survey.Ask(qs, c); err != nil {
+ return c, err
+ }
+ }
+ return c, err
+}
+
+func (c buildConfig) buildclientOptions() ([]dubbo.Option, error) {
+ var o []dubbo.Option
+
+ if c.UseDockerfile {
+ o = append(o, dubbo.WithBuilder(dockerfile.NewBuilder()))
+ } else {
+ o = append(o, dubbo.WithBuilder(pack.NewBuilder()))
+ }
+
+ return o, nil
+}
+
+type buildConfig struct {
+ Envs []string
+ Force bool
+ UseDockerfile bool
+ // Push the resulting image to the registry after building.
+ Push bool
+ // BuilderImage is the image (name or mapping) to use for building.
Usually
+ // set automatically.
+ BuilderImage string
+ Image string
+
+ // Path of the application implementation on local disk. Defaults to
current
+ // working directory of the process.
+ Path string
+}
+
+func newBuildConfig(cmd *cobra.Command) *buildConfig {
+ c := &buildConfig{
+ Envs: viper.GetStringSlice("envs"),
+ Force: viper.GetBool("force"),
+ UseDockerfile: viper.GetBool("useDockerfile"),
+ Push: viper.GetBool("push"),
+ BuilderImage: viper.GetString("builder-image"),
+ Image: viper.GetString("image"),
+ Path: viper.GetString("path"),
+ }
+
+ var err error
+ if c.Envs, err = cmd.Flags().GetStringArray("envs"); err != nil {
+ fmt.Fprintf(cmd.OutOrStdout(), "error reading envs: %v\n", err)
+ }
+ return c
+}
+
+func (c *buildConfig) Configure(f *dubbo.Dubbo) {
+ if c.Path == "" {
+ root, err := os.Getwd()
+ if err != nil {
+ return
+ }
+ f.Root = root
+ } else {
+ f.Root = c.Path
+ }
+ if c.BuilderImage != "" {
+ f.Build.BuilderImages["pack"] = c.BuilderImage
+ }
+ if c.Image != "" {
+ f.Image = c.Image
+ }
+
+ if len(c.Envs) > 0 {
+ envs := map[string]string{}
+ for _, env := range f.Build.BuildEnvs {
+ envs[*env.Name] = *env.Value
+ }
+ for _, pair := range c.Envs {
+ parts := strings.Split(pair, "=")
+ if len(parts) == 2 {
+ envs[parts[0]] = parts[1]
+ }
+ }
+ f.Build.BuildEnvs = make([]dubbo.Env, 0, len(envs))
+ for k, v := range envs {
+ f.Build.BuildEnvs = append(f.Build.BuildEnvs, dubbo.Env{
+ Name: &k,
+ Value: &v,
+ })
+ }
+ }
+}
diff --git a/dubboctl/cmd/client.go b/dubboctl/cmd/client.go
new file mode 100644
index 00000000..71894a8c
--- /dev/null
+++ b/dubboctl/cmd/client.go
@@ -0,0 +1,87 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "net/http"
+ "os"
+)
+
+import (
+ "github.com/apache/dubbo-kubernetes/dubboctl/cmd/prompt"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/builders/pack"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/docker"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/docker/creds"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+ dubbohttp "github.com/apache/dubbo-kubernetes/dubboctl/internal/http"
+ config "github.com/apache/dubbo-kubernetes/dubboctl/internal/util"
+)
+
+// ClientFactory defines a constructor which assists in the creation of a
Client
+// for use by commands.
+// See the NewClient constructor which is the fully populated ClientFactory
used
+// by commands by default.
+// See NewClientFactory which constructs a minimal ClientFactory for use
+// during testing.
+type ClientFactory func(...dubbo.Option) (*dubbo.Client, func())
+
+func NewClient(options ...dubbo.Option) (*dubbo.Client, func()) {
+ var (
+ t = newTransport(false)
+ c = newCredentialsProvider(config.Dir(), t)
+ d = newDubboDeployer()
+ o = []dubbo.Option{
+ dubbo.WithRepositoriesPath(config.RepositoriesPath()),
+ dubbo.WithBuilder(pack.NewBuilder()),
+ dubbo.WithPusher(docker.NewPusher(
+ docker.WithCredentialsProvider(c),
+ docker.WithTransport(t))),
+ dubbo.WithDeployer(d),
+ }
+ )
+ // Client is constructed with standard options plus any additional
options
+ // which either augment or override the defaults.
+ client := dubbo.New(append(o, options...)...)
+
+ cleanup := func() {}
+ return client, cleanup
+}
+
+func newDubboDeployer() dubbo.Deployer {
+ var options []dubbo.DeployerOpt
+
+ return dubbo.NewDeployer(options...)
+}
+
+// newTransport returns a transport with cluster-flavor-specific variations
+// which take advantage of additional features offered by cluster variants.
+func newTransport(insecureSkipVerify bool) dubbohttp.RoundTripCloser {
+ return
dubbohttp.NewRoundTripper(dubbohttp.WithInsecureSkipVerify(insecureSkipVerify))
+}
+
+// newCredentialsProvider returns a credentials provider which possibly
+// has cluster-flavor specific additional credential loaders to take advantage
+// of features or configuration nuances of cluster variants.
+func newCredentialsProvider(configPath string, t http.RoundTripper)
docker.CredentialsProvider {
+ options := []creds.Opt{
+
creds.WithPromptForCredentials(prompt.NewPromptForCredentials(os.Stdin,
os.Stdout, os.Stderr)),
+
creds.WithPromptForCredentialStore(prompt.NewPromptForCredentialStore()),
+ creds.WithTransport(t),
+ }
+
+ // Other cluster variants can be supported here
+ return creds.NewCredentialsProvider(configPath, options...)
+}
diff --git a/dubboctl/cmd/common.go b/dubboctl/cmd/common.go
new file mode 100644
index 00000000..5fa60c67
--- /dev/null
+++ b/dubboctl/cmd/common.go
@@ -0,0 +1,26 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var (
+ // TestInstallFlag and TestCli are uses for black box testing
+ TestInstallFlag bool
+ TestCli client.Client
+)
diff --git a/dubboctl/cmd/completion_util.go b/dubboctl/cmd/completion_util.go
new file mode 100644
index 00000000..96f4d8c6
--- /dev/null
+++ b/dubboctl/cmd/completion_util.go
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "strings"
+)
+
+import (
+ "github.com/spf13/cobra"
+)
+
+import (
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+)
+
+func CompleteRuntimeList(cmd *cobra.Command, args []string, toComplete string,
client *dubbo.Client) (matches []string, directive cobra.ShellCompDirective) {
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error listing runtimes for flag
completion: %v\n", err)
+ return
+ }
+ for _, runtime := range runtimes {
+ if strings.HasPrefix(runtime, toComplete) {
+ matches = append(matches, runtime)
+ }
+ }
+ return
+}
+
+func CompleteTemplateList(cmd *cobra.Command, args []string, toComplete
string, client *dubbo.Client) (matches []string, directive
cobra.ShellCompDirective) {
+ directive = cobra.ShellCompDirectiveError
+
+ lang, err := cmd.Flags().GetString("language")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cannot list templates: %v\n", err)
+ return
+ }
+ if lang == "" {
+ fmt.Fprintln(os.Stderr, "cannot list templates: language not
specified")
+ return
+ }
+
+ templates, err := client.Templates().List(lang)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cannot list templates: %v\n", err)
+ return
+ }
+
+ directive = cobra.ShellCompDirectiveDefault
+ for _, t := range templates {
+ if strings.HasPrefix(t, toComplete) {
+ matches = append(matches, t)
+ }
+ }
+
+ return
+}
diff --git a/dubboctl/cmd/create.go b/dubboctl/cmd/create.go
new file mode 100644
index 00000000..b3ff7387
--- /dev/null
+++ b/dubboctl/cmd/create.go
@@ -0,0 +1,572 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "text/tabwriter"
+ "text/template"
+)
+
+import (
+ "github.com/AlecAivazis/survey/v2"
+
+ "github.com/ory/viper"
+
+ "github.com/spf13/cobra"
+)
+
+import (
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/util"
+)
+
+// ErrNoRuntime indicates that the language runtime flag was not passed.
+type ErrNoRuntime error
+
+// ErrInvalidRuntime indicates that the passed language runtime was invalid.
+type ErrInvalidRuntime error
+
+// ErrInvalidTemplate indicates that the passed template was invalid.
+type ErrInvalidTemplate error
+
+// NewCreateCmd creates a create command using the given client creator.
+func addCreate(baseCmd *cobra.Command, newClient ClientFactory) {
+ cmd := &cobra.Command{
+ Use: "create",
+ Short: "Create an application",
+ Long: `
+NAME
+ {{.Name}} create - Create an application
+
+SYNOPSIS
+ {{.Name}} create [-l|--language] [-t|--template] [-r|--repository]
+ [-c|--confirm] [path]
+
+DESCRIPTION
+ Creates a new application.
+
+ $ {{.Name}} create -l go
+
+ Creates a function in the current directory '.' which is written in the
+ language/runtime 'go' common .
+
+ If [path] is provided, the function is initialized at that path,
creating
+ the path if necessary.
+
+ To complete this command interactively, use --confirm (-c):
+ $ {{.Name}} create -c
+
+ Initialize the current project directly into a dubbo project without
using a template
+ $ dubboctl create --init
+
+ Available Language Runtimes and Templates:
+{{ .Options | indent 2 " " | indent 1 "\t" }}
+
+ To install more language runtimes and their templates see '{{.Name}}
repository'.
+
+
+EXAMPLES
+ o Create a Node.js function in the current directory (the default path)
which
+ handles http events (the default template).
+ $ {{.Name}} create -l java
+
+ o Create a java common in the directory 'mydubbo'.
+ $ {{.Name}} create -l java mydubbo
+
+ o Create a Main common in ./mydubbo.
+ $ {{.Name}} create -l go -t common mydubbo
+ `,
+ SuggestFor: []string{"vreate", "creaet", "craete", "new"},
+ PreRunE: bindEnv("language", "template", "repository",
"confirm", "init"),
+ Aliases: []string{"init"},
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runCreate(cmd, args, newClient)
+ },
+ }
+
+ // Flags
+ cmd.Flags().StringP("language", "l", "", "Language Runtime (see help
text for list) ($DUBBO_LANGUAGE)")
+ cmd.Flags().StringP("template", "t", "", "Application template. (see
help text for list) ($DUBBO_TEMPLATE)")
+ cmd.Flags().StringP("repository", "r", "", "URI to a Git repository
containing the specified template ($DUBBO_REPOSITORY)")
+ cmd.Flags().BoolP("init", "i", false,
+ "Initialize the current project directly into a dubbo project
without using a template")
+
+ addConfirmFlag(cmd, false)
+
+ // Help Action
+ cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
runCreateHelp(cmd, args, newClient) })
+
+ // Tab completion
+ if err := cmd.RegisterFlagCompletionFunc("language",
newRuntimeCompletionFunc(newClient)); err != nil {
+ fmt.Fprintf(os.Stderr, "unable to provide language runtime
suggestions: %v\n", err)
+ }
+ if err := cmd.RegisterFlagCompletionFunc("template",
newTemplateCompletionFunc(newClient)); err != nil {
+ fmt.Fprintf(os.Stderr, "unable to provide template suggestions:
%v\n", err)
+ }
+
+ baseCmd.AddCommand(cmd)
+}
+
+// Run Create
+func runCreate(cmd *cobra.Command, args []string, newClient ClientFactory)
(err error) {
+ // Config
+ // Create a config based on args. Also uses the newClient to create a
+ // temporary client for completing options such as available runtimes.
+ cfg, err := newCreateConfig(cmd, args, newClient)
+ if err != nil {
+ return
+ }
+
+ // Client
+ // From environment variables, flags, arguments, and user prompts if
--confirm
+ // (in increasing levels of precedence)
+ client, done := newClient(
+ dubbo.WithRepository(cfg.Repository))
+ defer done()
+
+ // Validate - a deeper validation than that which is performed when
+ // instantiating the client with the raw config above.
+ if err = cfg.Validate(client); err != nil {
+ return
+ }
+
+ // Create
+ _, err = client.Init(&dubbo.Dubbo{
+ Name: cfg.Name,
+ Root: cfg.Path,
+ Runtime: cfg.Runtime,
+ Template: cfg.Template,
+ Build: dubbo.BuildSpec{
+ CnMirror: dubbo.BooleanWithComment{
+ Comment: "Specify `cnMirror: true` to use the
mirror in mainland China",
+ },
+ },
+ }, cfg.Init, cmd)
+ if err != nil {
+ return err
+ }
+
+ // Confirm
+ fmt.Fprintf(cmd.OutOrStderr(), "Created %v dubbo application in %v\n",
cfg.Runtime, cfg.Path)
+ return nil
+}
+
+type createConfig struct {
+ Path string // Absolute path to function source
+ Runtime string // Language Runtime
+ Repository string // Repository URI (overrides builtin and installed)
+ Confirm bool // Confirm values via an interactive prompt
+
+ // like common is a template
+ Template string
+
+ // Name of the function
+ Name string
+
+ Init bool
+}
+
+// newCreateConfig returns a config populated from the current execution
context
+// (args, flags and environment variables)
+// The client constructor function is used to create a transient client for
+// accessing things like the current valid templates list, and uses the
+// current value of the config at time of prompting.
+func newCreateConfig(cmd *cobra.Command, args []string, newClient
ClientFactory) (cfg createConfig, err error) {
+ var (
+ path string
+ dirName string
+ absolutePath string
+ )
+
+ if len(args) >= 1 {
+ path = args[0]
+ }
+
+ dirName, absolutePath = deriveNameAndAbsolutePathFromPath(path)
+
+ // Config is the final default values based off the execution context.
+ // When prompting, these become the defaults presented.
+ cfg = createConfig{
+ Name: dirName,
+ Path: absolutePath,
+ Repository: viper.GetString("repository"),
+ Runtime: viper.GetString("language"), // users refer to it
is language
+ Template: viper.GetString("template"),
+ Confirm: viper.GetBool("confirm"),
+ Init: viper.GetBool("init"),
+ }
+ // If not in confirm/prompting mode, this cfg structure is complete.
+ if !cfg.Confirm {
+ return
+ }
+
+ // Create a temporary client for use by the following prompts to
complete
+ // runtime/template suggestions etc
+ client, done := newClient()
+ defer done()
+
+ // IN confirm mode. If also in an interactive terminal, run prompts.
+ if util.InteractiveTerminal() {
+ createdCfg, err := cfg.prompt(client)
+ if err != nil {
+ return createdCfg, err
+ }
+ fmt.Println("Command:")
+ fmt.Println(singleCommand(cmd, args, createdCfg))
+ return createdCfg, nil
+ }
+
+ // Confirming, but noninteractive
+ // Print out the final values as a confirmation. Only show Repository
or
+ // Repositories, not both (repository takes precedence) in order to
avoid
+ // likely confusion if both are displayed and one is empty.
+ // be removed and both displayed.
+ fmt.Printf("Path: %v\n", cfg.Path)
+ fmt.Printf("Language: %v\n", cfg.Runtime) // users refer to it as
language
+ if cfg.Repository != "" { // if an override was
provided
+ fmt.Printf("Repository: %v\n", cfg.Repository) // show only
the override
+ }
+ fmt.Printf("Template: %v\n", cfg.Template)
+ return
+}
+
+// singleCommand that could be used by the current user to minimally recreate
the current state.
+func singleCommand(cmd *cobra.Command, args []string, cfg createConfig) string
{
+ var b strings.Builder
+ b.WriteString(cmd.Root().Name()) // process executable
+ b.WriteString(" -l " + cfg.Runtime) // language runtime is required
+ if cmd.Flags().Lookup("template").Changed {
+ b.WriteString(" -t " + cfg.Template)
+ }
+ if cmd.Flags().Lookup("repository").Changed {
+ b.WriteString(" -r " + cfg.Repository)
+ }
+ if len(args) > 0 {
+ b.WriteString(" " + cfg.Path) // optional trailing <path>
argument
+ }
+ return b.String()
+}
+
+// Validate the current state of the config, returning any errors.
+// Note this is a deeper validation using a client already configured with a
+// preliminary config object from flags/config, such that the client instance
+// can be used to determine possible values for runtime, templates, etc. a
+// pre-client validation should not be required, as the Client does its own
+// validation.
+func (c createConfig) Validate(client *dubbo.Client) (err error) {
+ dirName, _ := deriveNameAndAbsolutePathFromPath(c.Path)
+ if err = util.ValidateApplicationName(dirName); err != nil {
+ return
+ }
+
+ if c.Runtime == "" {
+ return noRuntimeError(client)
+ }
+ if c.Runtime != "" && c.Repository == "" &&
+ !isValidRuntime(client, c.Runtime) {
+ return newInvalidRuntimeError(client, c.Runtime)
+ }
+
+ if c.Template != "" && c.Repository == "" &&
+ !isValidTemplate(client, c.Runtime, c.Template) {
+ return newInvalidTemplateError(client, c.Runtime, c.Template)
+ }
+
+ return
+}
+
+// isValidRuntime determines if the given language runtime is a valid choice.
+func isValidRuntime(client *dubbo.Client, runtime string) bool {
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ return false
+ }
+ for _, v := range runtimes {
+ if v == runtime {
+ return true
+ }
+ }
+ return false
+}
+
+// isValidTemplate determines if the given template is valid for the given
+// runtime.
+func isValidTemplate(client *dubbo.Client, runtime, template string) bool {
+ if !isValidRuntime(client, runtime) {
+ return false
+ }
+ templates, err := client.Templates().List(runtime)
+ if err != nil {
+ return false
+ }
+ for _, v := range templates {
+ if v == template {
+ return true
+ }
+ }
+ return false
+}
+
+func noRuntimeError(client *dubbo.Client) error {
+ b := strings.Builder{}
+ fmt.Fprintln(&b, "Required flag \"language\" not set.")
+ fmt.Fprintln(&b, "Available language runtimes are:")
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ return err
+ }
+ for _, v := range runtimes {
+ fmt.Fprintf(&b, " %v\n", v)
+ }
+ return ErrNoRuntime(errors.New(b.String()))
+}
+
+func newInvalidRuntimeError(client *dubbo.Client, runtime string) error {
+ b := strings.Builder{}
+ fmt.Fprintf(&b, "The language runtime '%v' is not recognized.\n",
runtime)
+ fmt.Fprintln(&b, "Available language runtimes are:")
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ return err
+ }
+ for _, v := range runtimes {
+ fmt.Fprintf(&b, " %v\n", v)
+ }
+ return ErrInvalidRuntime(errors.New(b.String()))
+}
+
+func newInvalidTemplateError(client *dubbo.Client, runtime, template string)
error {
+ b := strings.Builder{}
+ fmt.Fprintf(&b, "The template '%v' was not found for language runtime
'%v'.\n", template, runtime)
+ fmt.Fprintln(&b, "Available templates for this language runtime are:")
+ templates, err := client.Templates().List(runtime)
+ if err != nil {
+ return err
+ }
+ for _, v := range templates {
+ fmt.Fprintf(&b, " %v\n", v)
+ }
+ return ErrInvalidTemplate(errors.New(b.String()))
+}
+
+// prompt the user with value of config members, allowing for interactively
+// mutating the values. The provided clientFn is used to construct a transient
+// client for use during prompt autocompletion/suggestions (such as suggesting
+// valid templates)
+func (c createConfig) prompt(client *dubbo.Client) (createConfig, error) {
+ var qs []*survey.Question
+
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ return createConfig{}, err
+ }
+
+ init := false
+
+ // ask for init
+ qs = []*survey.Question{
+ {
+ Name: "Init",
+ Prompt: &survey.Confirm{
+ Message: "Create a new dubbo project or
directly initialize it into a dubbo project",
+ Default: init,
+ },
+ },
+ }
+ if err = survey.Ask(qs, &init); err != nil {
+ return createConfig{}, err
+ }
+
+ // ask for path...
+ qs = []*survey.Question{
+ {
+ Name: "Path",
+ Prompt: &survey.Input{
+ Message: "Scaffold Path:",
+ Default: c.Path,
+ },
+ Validate: func(val interface{}) error {
+ derivedName, _ :=
deriveNameAndAbsolutePathFromPath(val.(string))
+ return util.ValidateApplicationName(derivedName)
+ },
+ Transform: func(ans interface{}) interface{} {
+ _, absolutePath :=
deriveNameAndAbsolutePathFromPath(ans.(string))
+ return absolutePath
+ },
+ }, {
+ Name: "Runtime",
+ Prompt: &survey.Select{
+ Message: "Language Runtime:",
+ Options: runtimes,
+ Default: surveySelectDefault(c.Runtime,
runtimes),
+ },
+ },
+ }
+ if err := survey.Ask(qs, &c); err != nil {
+ return c, err
+ }
+
+ if !init {
+ // Second loop: choose template with autocompletion filtered by
chosen runtime
+ qs = []*survey.Question{
+ {
+ Name: "Template",
+ Prompt: &survey.Input{
+ Message: "Template:",
+ Default: c.Template,
+ Suggest: func(prefix string) []string {
+ suggestions, err :=
templatesWithPrefix(prefix, c.Runtime, client)
+ if err != nil {
+ fmt.Fprintf(os.Stderr,
"unable to suggest: %v\n", err)
+ }
+ return suggestions
+ },
+ },
+ },
+ }
+ if err := survey.Ask(qs, &c); err != nil {
+ return c, err
+ }
+ }
+ return c, nil
+}
+
+// Tab Completion and Prompt Suggestions Helpers
+// ---------------------------------------------
+
+type flagCompletionFunc func(*cobra.Command, []string, string) ([]string,
cobra.ShellCompDirective)
+
+func newRuntimeCompletionFunc(newClient ClientFactory) flagCompletionFunc {
+ return func(cmd *cobra.Command, args []string, toComplete string)
([]string, cobra.ShellCompDirective) {
+ _, err := newCreateConfig(cmd, args, newClient)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error creating client config
for flag completion: %v\n", err)
+ }
+ client, done := newClient()
+ defer done()
+ return CompleteRuntimeList(cmd, args, toComplete, client)
+ }
+}
+
+func newTemplateCompletionFunc(newClient ClientFactory) flagCompletionFunc {
+ return func(cmd *cobra.Command, args []string, toComplete string)
([]string, cobra.ShellCompDirective) {
+ _, err := newCreateConfig(cmd, args, newClient)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error creating client config
for flag completion: %v\n", err)
+ }
+ client, done := newClient()
+ defer done()
+ return CompleteTemplateList(cmd, args, toComplete, client)
+ }
+}
+
+// return templates for language runtime whose full name (including repository)
+// have the given prefix.
+func templatesWithPrefix(prefix, runtime string, client *dubbo.Client)
([]string, error) {
+ var (
+ suggestions []string
+ templates, err = client.Templates().List(runtime)
+ )
+ if err != nil {
+ return suggestions, err
+ }
+ for _, template := range templates {
+ if strings.HasPrefix(template, prefix) {
+ suggestions = append(suggestions, template)
+ }
+ }
+ return suggestions, nil
+}
+
+// runCreateHelp prints help for the create command using a template
+// and options.
+func runCreateHelp(cmd *cobra.Command, args []string, newClient ClientFactory)
{
+ failSoft := func(err error) {
+ if err != nil {
+ fmt.Fprintf(cmd.OutOrStderr(), "error: help text may be
partial: %v\n", err)
+ }
+ }
+
+ tpl := newHelpTemplate(cmd)
+
+ cfg, err := newCreateConfig(cmd, args, newClient)
+ failSoft(err)
+
+ client, done := newClient(
+ dubbo.WithRepository(cfg.Repository))
+ defer done()
+
+ options, err := RuntimeTemplateOptions(client) // human-friendly
+ failSoft(err)
+
+ data := struct {
+ Options string
+ Name string
+ }{
+ Options: options,
+ Name: cmd.Root().Use,
+ }
+
+ if err := tpl.Execute(cmd.OutOrStdout(), data); err != nil {
+ fmt.Fprintf(cmd.ErrOrStderr(), "unable to display help text:
%v\n", err)
+ }
+}
+
+// newHelpTemplate returns a template for the create command's help text
+func newHelpTemplate(cmd *cobra.Command) *template.Template {
+ body := cmd.Long + "\n\n" + cmd.UsageString()
+ t := template.New("help")
+ fm := template.FuncMap{
+ "indent": func(i int, c string, v string) string {
+ indentation := strings.Repeat(c, i)
+ return indentation + strings.Replace(v, "\n",
"\n"+indentation, -1)
+ },
+ }
+ t.Funcs(fm)
+ return template.Must(t.Parse(body))
+}
+
+// RuntimeTemplateOptions is a human-friendly table of valid Language Runtime
+// to Template combinations.
+// Exported for use in docs.
+func RuntimeTemplateOptions(client *dubbo.Client) (string, error) {
+ runtimes, err := client.Runtimes()
+ if err != nil {
+ return "", err
+ }
+ builder := strings.Builder{}
+ writer := tabwriter.NewWriter(&builder, 0, 0, 3, ' ', 0)
+
+ fmt.Fprint(writer, "Language\tTemplate\n")
+ fmt.Fprint(writer, "--------\t--------\n")
+ for _, r := range runtimes {
+ templates, err := client.Templates().List(r)
+ // Not all language packs will have templates for
+ // all available runtimes. Without this check
+ if err != nil && !errors.Is(err, dubbo.ErrTemplateNotFound) {
+ return "", err
+ }
+ for _, t := range templates {
+ fmt.Fprintf(writer, "%v\t%v\n", r, t) // write tabbed
+ }
+ }
+ writer.Flush()
+ return builder.String(), nil
+}
diff --git a/dubboctl/cmd/deploy.go b/dubboctl/cmd/deploy.go
new file mode 100644
index 00000000..2d27385b
--- /dev/null
+++ b/dubboctl/cmd/deploy.go
@@ -0,0 +1,317 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+import (
+ "github.com/AlecAivazis/survey/v2"
+
+ "github.com/ory/viper"
+
+ "github.com/spf13/cobra"
+
+ "k8s.io/client-go/rest"
+
+ "k8s.io/client-go/tools/clientcmd"
+
+ "k8s.io/client-go/util/homedir"
+)
+
+import (
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/kube"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/util"
+)
+
+const (
+ basePort = 30000
+ portLimit = 32767
+)
+
+func addDeploy(baseCmd *cobra.Command, newClient ClientFactory) {
+ cmd := &cobra.Command{
+ Use: "deploy",
+ Short: "Generate the k8s yaml of the application. By the way,
you can choose to build the image, push the image and apply to the k8s
cluster.",
+ Long: `
+NAME
+ dubboctl deploy - Generate the k8s yaml of the application. By the way,
you can choose to build the image, push the image and apply to the k8s cluster.
+
+SYNOPSIS
+ dubboctl deploy [flags]
+`,
+ SuggestFor: []string{"delpoy", "deplyo"},
+ PreRunE: bindEnv("path", "output", "namespace", "image",
"envs", "name", "containerPort",
+ "targetPort", "nodePort", "apply", "useDockerfile",
"force", "builder-image", "build", "context",
+ "kubeConfig", "push"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runDeploy(cmd, newClient)
+ },
+ }
+ cmd.Flags().StringP("namespace", "n", "default",
+ "Deploy into a specific namespace")
+ cmd.Flags().StringP("output", "o", "kube.yaml",
+ "output kubernetes manifest")
+ cmd.Flags().StringP("name", "", "",
+ "The name of application")
+ cmd.Flags().IntP("containerPort", "", 0,
+ "The port of the deployment to listen on pod (required)")
+ cmd.Flags().IntP("targetPort", "", 0,
+ "The targetPort of the deployment, default to port")
+ cmd.Flags().IntP("nodePort", "", 0,
+ "The nodePort of the deployment to expose")
+
+ cmd.Flags().StringP("context", "", "",
+ "Context in kubeconfig to use")
+ cmd.Flags().StringP("kubeConfig", "k", "",
+ "Path to kubeconfig")
+
+ cmd.Flags().StringArrayP("envs", "e", nil,
+ "DeployMode variable to set in the form NAME=VALUE. "+
+ "This is for the environment variables passed in by the
builderpack build method.")
+ cmd.Flags().StringP("builder-image", "b", "",
+ "Specify a custom builder image for use by the builder other
than its default.")
+ cmd.Flags().BoolP("useDockerfile", "d", false,
+ "Use the dockerfile with the specified path to build")
+ cmd.Flags().StringP("image", "i", "",
+ "Container image( [registry]/[namespace]/[name]:[tag] )")
+ cmd.Flags().BoolP("push", "", true,
+ "Whether to push the image to the registry center by the way")
+ cmd.Flags().BoolP("force", "f", false,
+ "Whether to force build")
+
+ cmd.Flags().BoolP("build", "", true,
+ "Whether to build the image")
+ cmd.Flags().BoolP("apply", "a", false,
+ "Whether to apply the application to the k8s cluster by the
way")
+ cmd.Flags().StringP("portName", "", "http",
+ "Name of the port to be exposed")
+
+ addPathFlag(cmd)
+ cmd.Flags().SetInterspersed(false)
+ baseCmd.AddCommand(cmd)
+}
+
+func runDeploy(cmd *cobra.Command, newClient ClientFactory) error {
+ if err := util.CreatePaths(); err != nil {
+ return err
+ }
+ cfg := newDeployConfig(cmd)
+ f, err := dubbo.NewDubbo(cfg.Path)
+ if err != nil {
+ return err
+ }
+ cfg, err = cfg.Prompt(f)
+ if err != nil {
+ return err
+ }
+ if err := cfg.Validate(cmd); err != nil {
+ return err
+ }
+
+ if !f.Initialized() {
+ return dubbo.NewErrNotInitialized(f.Root)
+ }
+
+ cfg.Configure(f)
+
+ clientOptions, err := cfg.deployclientOptions()
+ if err != nil {
+ return err
+ }
+ client, done := newClient(clientOptions...)
+ defer done()
+
+ kubeEnv := true
+ _, err = rest.InClusterConfig()
+ if err != nil {
+ kubeconfig := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
+ if len(kubeconfig) <= 0 {
+ if home := homedir.HomeDir(); home != "" {
+ kubeconfig = filepath.Join(home, ".kube",
"config")
+ }
+ }
+ _, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
+ if err != nil {
+ kubeEnv = false
+ }
+ }
+
+ if kubeEnv {
+ err := f.CheckLabels(cfg.Namespace, client)
+ if err != nil {
+ return err
+ }
+ }
+
+ // generate template first
+ f, err = client.Deploy(cmd.Context(), f)
+ if err != nil {
+ return err
+ }
+
+ if cfg.Build {
+ if f.Built() && !cfg.Force {
+ fmt.Fprintf(cmd.OutOrStdout(), "The Application is up
to date, If you still want to build, use `--force true`\n")
+ return nil
+ }
+ if f, err = client.Build(cmd.Context(), f); err != nil {
+ return err
+ }
+ if cfg.Push {
+ if f, err = client.Push(cmd.Context(), f); err != nil {
+ return err
+ }
+ }
+ }
+
+ if cfg.Apply {
+ err := applyTok8s(cmd, f)
+ if err != nil {
+ return err
+ }
+ }
+
+ if err = f.Write(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d DeployConfig) deployclientOptions() ([]dubbo.Option, error) {
+ o, err := d.buildclientOptions()
+ if err != nil {
+ return o, err
+ }
+ var cliOpts []kube.CtlClientOption
+ cliOpts = []kube.CtlClientOption{
+ kube.WithKubeConfigPath(d.KubeConfig),
+ kube.WithContext(d.Context),
+ }
+ cli, err := kube.NewCtlClient(cliOpts...)
+ if err != nil {
+ return o, err
+ }
+ o = append(o, dubbo.WithKubeClient(cli))
+ return o, nil
+}
+
+func applyTok8s(cmd *cobra.Command, d *dubbo.Dubbo) error {
+ file := filepath.Join(d.Root, d.Deploy.Output)
+ c := exec.CommandContext(cmd.Context(), "kubectl", "apply", "-f", file)
+ c.Stdout = os.Stdout
+ c.Stderr = os.Stderr
+ err := c.Run()
+ return err
+}
+
+func (c DeployConfig) Validate(cmd *cobra.Command) (err error) {
+ nodePort := c.NodePort
+ if nodePort != 0 && (nodePort < basePort || nodePort > portLimit) {
+ return errors.New("nodePort should be between 30000 and 32767")
+ }
+ return nil
+}
+
+func (c *DeployConfig) Prompt(d *dubbo.Dubbo) (*DeployConfig, error) {
+ var err error
+ if !util.InteractiveTerminal() {
+ return c, nil
+ }
+ buildconfig, err := c.buildConfig.Prompt(d)
+ if err != nil {
+ return c, err
+ }
+ c.buildConfig = buildconfig
+
+ if d.Deploy.ContainerPort == 0 && c.ContainerPort == 0 {
+ qs := []*survey.Question{
+ {
+ Name: "containerPort",
+ Validate: survey.Required,
+ Prompt: &survey.Input{
+ Message: "The container port",
+ },
+ },
+ }
+ if err = survey.Ask(qs, c); err != nil {
+ return c, err
+ }
+ }
+ return c, err
+}
+
+func (c DeployConfig) Configure(f *dubbo.Dubbo) {
+ c.buildConfig.Configure(f)
+ if c.Namespace != "" {
+ f.Deploy.Namespace = c.Namespace
+ }
+ if c.Output != "" {
+ f.Deploy.Output = c.Output
+ }
+ if c.ContainerPort != 0 {
+ f.Deploy.ContainerPort = c.ContainerPort
+ }
+ if c.TargetPort != 0 {
+ f.Deploy.TargetPort = c.TargetPort
+ }
+ if c.NodePort != 0 {
+ f.Deploy.NodePort = c.NodePort
+ }
+ if c.PortName != "" {
+ f.Deploy.PortName = c.PortName
+ }
+}
+
+type DeployConfig struct {
+ *buildConfig
+ KubeConfig string
+ Context string
+ Build bool
+ Apply bool
+ Namespace string
+ ContainerPort int
+ Output string
+ Force bool
+ TargetPort int
+ NodePort int
+ PortName string
+}
+
+func newDeployConfig(cmd *cobra.Command) (c *DeployConfig) {
+ c = &DeployConfig{
+ buildConfig: newBuildConfig(cmd),
+ KubeConfig: viper.GetString("kubeConfig"),
+ Context: viper.GetString("context"),
+ Build: viper.GetBool("build"),
+ Apply: viper.GetBool("apply"),
+ Output: viper.GetString("output"),
+ Namespace: viper.GetString("namespace"),
+ Force: viper.GetBool("force"),
+ ContainerPort: viper.GetInt("containerPort"),
+ TargetPort: viper.GetInt("targetPort"),
+ NodePort: viper.GetInt("nodePort"),
+ PortName: viper.GetString("portName"),
+ }
+ return
+}
diff --git a/dubboctl/cmd/repository.go b/dubboctl/cmd/repository.go
new file mode 100644
index 00000000..2714d5f2
--- /dev/null
+++ b/dubboctl/cmd/repository.go
@@ -0,0 +1,596 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+ "errors"
+ "fmt"
+ "os"
+)
+
+import (
+ "github.com/AlecAivazis/survey/v2"
+
+ "github.com/ory/viper"
+
+ "github.com/spf13/cobra"
+)
+
+import (
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/dubbo"
+ "github.com/apache/dubbo-kubernetes/dubboctl/internal/util"
+)
+
+// command constructors
+// --------------------
+func addRepository(baseCmd *cobra.Command, newClient ClientFactory) {
+ cmd := &cobra.Command{
+ Short: "Manage installed template repositories",
+ Use: "repository",
+ Aliases: []string{"repo", "repositories"},
+ Long: `
+NAME
+ dubboctl - Manage set of installed repositories.
+
+SYNOPSIS
+ dubboctl repo [-c|--confirm]
+ dubboctl repo list [-r|--repositories] [-c|--confirm]
+ dubboctl repo add <name> <url>[-r|--repositories] [-c|--confirm]
+ dubboctl repo rename <old> <new> [-r|--repositories] [-c|--confirm]
+ dubboctl repo remove <name> [-r|--repositories] [-c|--confirm]
+
+DESCRIPTION
+ Manage template repositories installed on disk at either the default
location
+ (~/.config/dubbo/repositories) or the location specified by the
--repository
+ flag. Once added, a template from the repository can be used when
creating
+ a new Dubbo.
+
+ Interactive Prompts:
+ To complete these commands interactively, pass the --confirm (-c) flag
to
+ the 'repository' command, or any of the inidivual subcommands.
+
+ The Default Repository:
+ The default repository is not stored on disk, but embedded in the
binary and
+ can be used without explicitly specifying the name. The default
repository
+ is always listed first, and is assumed when creating a new function
without
+ specifying a repository name prefix.
+ For example, to create a new one using the 'common' template from the
+ default repository.
+ $ dubboctl create -l go -t common
+
+ The Repository Flag:
+ Installing repositories locally is optional. To use a template from a
remote
+ repository directly, it is possible to use the --repository flag on
create.
+ This leaves the local disk untouched. For example, To create a
scaffold using
+ the dubboctl-samples http template without installing the template
+ repository locally, use the --repository (-r) flag on create:
+ $ dubboctl create -l go \
+ --template http \
+ --repository https://github.com/sjmshsh/dubboctl-samples
+
+ Alternative Repositories Location:
+ Repositories are stored on disk in ~/.config/dubbo/repositories by
default.
+ This location can be altered by setting the DUBBO_REPOSITORIES_PATH
+ environment variable.
+
+
+COMMANDS
+
+ With no arguments, this help text is shown. To manage repositories with
+ an interactive prompt, use the use the --confirm (-c) flag.
+ $ dubboctl repository -c
+
+ add
+ Add a new repository to the installed set.
+ $ dubboctl repository add <name> <URL>
+
+ For Example, to add the ruiyi Project repository:
+ $ dubboctl repository add ruiyi
https://github.com/sjmshsh/dubboctl-samples
+
+ Once added, a function can be created with templates from the new
repository
+ by prefixing the template name with the repository. For example, to
create
+ a new function using the dubbogo template:
+ $ dubboctl create -l go -t ruiyi/dubbogo
+
+ list
+ List all available repositories, including the installed default
+ repository. Repositories available are listed by name.
+
+ rename
+ Rename a previously installed repository from <old> to <new>. Only
installed
+ repositories can be renamed.
+ $ dubboctl repository rename <name> <new name>
+
+ remove
+ Remove a repository by name. Removes the repository from local
storage
+ entirely. When in confirm mode (--confirm) it will confirm before
+ deletion, but in regular mode this is done immediately, so please use
+ caution, especially when using an altered repositories location
+ (via the DUBBO_REPOSITORIES_PATH environment variable).
+ $ dubboctl repository remove <name>
+
+EXAMPLES
+ o Run in confirmation mode (interactive prompts) using the --confirm
flag
+ $ dubboctl repository -c
+
+ o Add a repository and create a new function using a template from it:
+ $ dubboctl repository add ruiyi
https://github.com/sjmshsh/dubboctl-samples
+ $ dubboctl repository list
+ default
+ functastic
+ $ dubboctl create -l go -t ruiyi/dubbogo
+ ...
+
+ o Add a repository specifying the branch to use (dubboctl):
+ $ dubboctl repository add ruiyi
https://github.com/sjmshsh/dubboctl-samples#dubboctl
+ $ dubboctl repository list
+ default
+ ruiyi
+ $ dubboctl create -l go -t http
+ ...
+
+ o List all repositories including the URL from which remotes were
installed
+ $ dubboctl repository list -v
+ default
+ master https://github.com/sjmshsh/dubboctl-samples#master
+
+ o Rename an installed repository
+ $ dubboctl repository list
+ default
+ ruiyi
+ $ dubboctl repository rename ruiyi dubboTest
+ $ dubboctl repository list
+ default
+ dubboTest
+
+ o Remove an installed repository
+ $ dubboctl repository list
+ default
+ dubboTest
+ $ dubboctl repository remove dubboTest
+ $ dubboctl repository list
+ default
+`,
+ SuggestFor: []string{"repositories", "repos", "template",
"templates", "pack", "packs"},
+ PreRunE: bindEnv("confirm"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRepository(cmd, args, newClient)
+ },
+ }
+
+ addConfirmFlag(cmd, false)
+
+ cmd.AddCommand(NewRepositoryListCmd(newClient))
+ cmd.AddCommand(NewRepositoryAddCmd(newClient))
+ cmd.AddCommand(NewRepositoryRenameCmd(newClient))
+ cmd.AddCommand(NewRepositoryRemoveCmd(newClient))
+
+ baseCmd.AddCommand(cmd)
+}
+
+func NewRepositoryListCmd(newClient ClientFactory) *cobra.Command {
+ cmd := &cobra.Command{
+ Short: "List repositories",
+ Use: "list",
+ Aliases: []string{"ls"},
+ PreRunE: bindEnv("confirm"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRepositoryList(cmd, args, newClient)
+ },
+ }
+
+ addConfirmFlag(cmd, false)
+ return cmd
+}
+
+func NewRepositoryAddCmd(newClient ClientFactory) *cobra.Command {
+ cmd := &cobra.Command{
+ Short: "Add a repository",
+ Use: "add <name> <url>",
+ SuggestFor: []string{"ad", "install"},
+ PreRunE: bindEnv("confirm"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRepositoryAdd(cmd, args, newClient)
+ },
+ }
+
+ addConfirmFlag(cmd, false)
+ return cmd
+}
+
+func NewRepositoryRenameCmd(newClient ClientFactory) *cobra.Command {
+ cmd := &cobra.Command{
+ Short: "Rename a repository",
+ Use: "rename <old> <new>",
+ Aliases: []string{"mv"},
+ PreRunE: bindEnv("confirm"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRepositoryRename(cmd, args, newClient)
+ },
+ }
+
+ addConfirmFlag(cmd, false)
+ return cmd
+}
+
+func NewRepositoryRemoveCmd(newClient ClientFactory) *cobra.Command {
+ cmd := &cobra.Command{
+ Short: "Remove a repository",
+ Use: "remove <name>",
+ Aliases: []string{"rm"},
+ SuggestFor: []string{"delete", "del"},
+ PreRunE: bindEnv("confirm"),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runRepositoryRemove(cmd, args, newClient)
+ },
+ }
+
+ addConfirmFlag(cmd, false)
+ return cmd
+}
+
+// command implementations
+// -----------------------
+
+// Run
+// (list by default or interactive with -c|--confirm)
+func runRepository(cmd *cobra.Command, args []string, newClient ClientFactory)
(err error) {
+ cfg, err := newRepositoryConfig(args)
+ if err != nil {
+ return
+ }
+
+ // If in noninteractive, normal mode the help text is shown
+ if !cfg.Confirm {
+ return cmd.Help()
+ }
+
+ // If in interactive mode, the user chan choose which subcommand to
invoke
+ // Prompt for action to perform
+ question := &survey.Question{
+ Name: "Action",
+ Prompt: &survey.Select{
+ Message: "Operation to perform:",
+ Options: []string{"list", "add", "rename", "remove"},
+ Default: "list",
+ },
+ }
+ answer := struct{ Action string }{}
+ if err = survey.Ask([]*survey.Question{question}, &answer); err != nil {
+ return
+ }
+
+ // Run the command indicated
+ switch answer.Action {
+ case "list":
+ return runRepositoryList(cmd, args, newClient)
+ case "add":
+ return runRepositoryAdd(cmd, args, newClient)
+ case "rename":
+ return runRepositoryRename(cmd, args, newClient)
+ case "remove":
+ return runRepositoryRemove(cmd, args, newClient)
+ }
+ return fmt.Errorf("invalid action '%v'", answer.Action) // Unreachable
+}
+
+// List
+func runRepositoryList(_ *cobra.Command, args []string, newClient
ClientFactory) (err error) {
+ _, err = newRepositoryConfig(args)
+ if err != nil {
+ return
+ }
+
+ client, done := newClient()
+ defer done()
+
+ // List all repositories given a client instantiated about config.
+ rr, err := client.Repositories().All()
+ if err != nil {
+ return
+ }
+
+ // Print repository names, or name plus url
+ // This follows the format of `git remote`, as it is likely familiar.
+ for _, r := range rr {
+ fmt.Fprintln(os.Stdout, r.Name+"\t"+r.URL())
+ }
+ return
+}
+
+// Add
+func runRepositoryAdd(_ *cobra.Command, args []string, newClient
ClientFactory) (err error) {
+ // Supports both composable, discrete CLI commands or prompt-based
"config"
+ // by setting the argument values (name and ulr) to value of positional
args,
+ // but only requires them if not prompting. If prompting, those values
+ // become the prompt defaults.
+
+ cfg, err := newRepositoryConfig(args)
+ if err != nil {
+ return
+ }
+
+ // Adding a repository requires there be a config path structure on disk
+ if err = util.CreatePaths(); err != nil {
+ return
+ }
+
+ // Create a client instance which utilizes the given repositories path.
+ // Note that this MAY not be in the config structure if the environment
+ // variable to override said path was provided explicitly.
+ // be created in XDG_CONFIG_HOME/dubbo even if the repo path environment
+ // was set to some other location on disk.
+ client, done := newClient()
+ defer done()
+
+ // Preconditions
+ // If not confirming/prompting, assert the args were both provided.
+ if len(args) != 2 && !cfg.Confirm {
+ return fmt.Errorf("usage: dubbo repository add <name> <url>")
+ }
+
+ // Extract Params
+ // Populate a struct with the arguments (if provided)
+ params := struct {
+ Name string
+ URL string
+ }{}
+ if len(args) > 0 {
+ params.Name = args[0]
+ }
+ if len(args) > 1 {
+ params.URL = args[1]
+ }
+
+ // Prompt/Confirm
+ // If confirming/prompting, interactively populate the params from the
user
+ // (using the current values as defaults)
+ //
+ // If terminal not interactive, effective values are echoed.
+ //
+ // Note that empty values can be passed to the final client's Add
method if:
+ // Argument(s) not provided
+ // Confirming (-c|--confirm)
+ // Is a noninteractive terminal
+ // This is an expected case. The empty value will be echoed to stdout,
the
+ // API will be invoked, and a helpful error message will indicate that
the
+ // request is missing required parameters.
+ if cfg.Confirm && util.InteractiveTerminal() {
+ questions := []*survey.Question{
+ {
+ Name: "Name",
+ Validate: survey.Required,
+ Prompt: &survey.Input{
+ Message: "Name for the new repository:",
+ Default: params.Name,
+ },
+ }, {
+ Name: "URL",
+ Validate: survey.Required,
+ Prompt: &survey.Input{
+ Message: "URL of the new repository:",
+ Default: params.URL,
+ },
+ },
+ }
+ if err = survey.Ask(questions, ¶ms); err != nil {
+ return
+ // not checking for terminal.InterruptError because
failure to complete,
+ // for whatever reason, should exit the program
non-zero.
+ }
+ } else if cfg.Confirm {
+ fmt.Fprintf(os.Stdout, "Name: %v\n", params.Name)
+ fmt.Fprintf(os.Stdout, "URL: %v\n", params.URL)
+ }
+
+ // Add repository
+ var n string
+ if n, err = client.Repositories().Add(params.Name, params.URL); err !=
nil {
+ return
+ }
+ fmt.Fprintf(os.Stdout, "Repository added: %s\n", n)
+ return
+}
+
+// Rename
+func runRepositoryRename(_ *cobra.Command, args []string, newClient
ClientFactory) (err error) {
+ cfg, err := newRepositoryConfig(args)
+ if err != nil {
+ return
+ }
+ client, done := newClient()
+ defer done()
+
+ // Preconditions
+ if len(args) != 2 && !cfg.Confirm {
+ return fmt.Errorf("usage: dubbo repository rename <old> <new>")
+ }
+
+ // Extract Params
+ params := struct {
+ Old string
+ New string
+ }{}
+ if len(args) > 0 {
+ params.Old = args[0]
+ }
+ if len(args) > 1 {
+ params.New = args[1]
+ }
+
+ // Repositories installed according to the client
+ // (does not include the builtin default)
+ repositories, err := installedRepositories(client)
+ if err != nil {
+ return
+ }
+
+ // Confirm (interactive prompt mode)
+ if cfg.Confirm && util.InteractiveTerminal() {
+ questions := []*survey.Question{
+ {
+ Name: "Old",
+ Validate: survey.Required,
+ Prompt: &survey.Select{
+ Message: "Repository to rename:",
+ Options: repositories,
+ },
+ }, {
+ Name: "New",
+ Validate: survey.Required,
+ Prompt: &survey.Input{
+ Message: "New name:",
+ Default: params.New,
+ },
+ },
+ }
+ if err = survey.Ask(questions, ¶ms); err != nil {
+ return // for any reason, including interrupt, is an
nonzero exit
+ }
+ } else if cfg.Confirm {
+ fmt.Fprintf(os.Stdout, "Old: %v\n", params.Old)
+ fmt.Fprintf(os.Stdout, "New: %v\n", params.New)
+ }
+
+ // Rename the repository
+ if err = client.Repositories().Rename(params.Old, params.New); err !=
nil {
+ return
+ }
+ fmt.Fprintln(os.Stdout, "Repository renamed")
+ return
+}
+
+// Remove
+func runRepositoryRemove(_ *cobra.Command, args []string, newClient
ClientFactory) (err error) {
+ cfg, err := newRepositoryConfig(args)
+ if err != nil {
+ return
+ }
+ client, done := newClient()
+ defer done()
+
+ // Preconditions
+ if len(args) != 1 && !cfg.Confirm {
+ return fmt.Errorf("usage: dubbo repository remove <name>")
+ }
+
+ // Extract param(s)
+ params := struct {
+ Name string
+ Sure bool
+ }{}
+ if len(args) > 0 {
+ params.Name = args[0]
+ }
+ // "Are you sure" confirmation flag
+ // (not using name 'Confirm' to avoid confusion with cfg.Confirm)
+ // defaults to Yes. This is debatable, but I don't want to choose the
repo
+ // to remove and then have to see a prompt and then have to hit 'y'.
Just
+ // prompting once to make sure, which requires another press of enter,
seems
+ // sufficient.
+ params.Sure = true
+
+ // Repositories installed according to the client
+ // (does not include the builtin default)
+ repositories, err := installedRepositories(client)
+ if err != nil {
+ return
+ }
+
+ if len(repositories) == 0 {
+ return errors.New("no repositories installed. use 'add' to
install")
+ }
+
+ // Confirm (interactive prompt mode)
+ if cfg.Confirm && util.InteractiveTerminal() {
+ questions := []*survey.Question{
+ {
+ Name: "Name",
+ Validate: survey.Required,
+ Prompt: &survey.Select{
+ Message: "Repository to remove:",
+ Options: repositories,
+ },
+ }, {
+ Name: "Sure",
+ Prompt: &survey.Confirm{
+ Message: "This will remove the
repository from local disk. Are you sure?",
+ Default: params.Sure,
+ },
+ },
+ }
+ if err = survey.Ask(questions, ¶ms); err != nil {
+ return // for any reason, including interrupt, is a
nonzero exit
+ }
+ } else if cfg.Confirm {
+ fmt.Fprintf(os.Stdout, "Repository: %v\n", params.Name)
+ }
+
+ // Cancel if they got cold feet.
+ if !params.Sure {
+ // While an argument could be made to the contrary, I believe
it is
+ // important than an abort by the user, either by answering no
to the
+ // confirmation or by an os interrupt such as ^C be considered
an error,
+ // and thus a non-zero program exit. This is because a user
may have
+ // chained the command, and an abort (for whatever reason)
should cancel
+ // the whole chain. For example, given the command:
+ // dubbo repo rm -cv && doSomethingOnSuccess
+ // The trailing command 'doSomethingOnSuccess' should not be
evaluated if
+ // the first, `dubbo repo rm`, does not exit 0.
+ fmt.Fprintln(os.Stdout, "Repository remove canceled")
+ return fmt.Errorf("repository removal canceled")
+ }
+
+ // Remove the repository
+ if err = client.Repositories().Remove(params.Name); err != nil {
+ return
+ }
+ fmt.Fprintln(os.Stdout, "Repository removed")
+ return
+}
+
+// Installed repositories
+// All repositories which have been installed (does not include builtin)
+func installedRepositories(client *dubbo.Client) ([]string, error) {
+ // Client API contract stipulates the list always lists the defeault
builtin
+ // repo, and always lists it at index 0
+ repositories, err := client.Repositories().List()
+ if err != nil {
+ return []string{}, err
+ }
+ return repositories[1:], nil
+}
+
+// client config
+// -------------
+
+// repositoryConfig used for instantiating a fn.Client
+type repositoryConfig struct {
+ Confirm bool // Enables interactive confirmation/prompting mode
+}
+
+// newRepositoryConfig creates a configuration suitable for use instantiating
the
+// fn Client. Note that parameters for the individual commands (add, remove
etc)
+// are collected separately in their requisite run functions.
+func newRepositoryConfig(args []string) (cfg repositoryConfig, err error) {
+ // initial config is populated based on flags, which are themselves
+ // first populated by static defaults, then environment variables,
+ // finally command flags.
+ cfg = repositoryConfig{
+ Confirm: viper.GetBool("confirm"),
+ }
+ return
+}