blob: b187480d90733c2481ca00e9a89d766ffdf9992c [file] [log] [blame]
// 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/app/dubboctl/internal/dubbo"
"github.com/apache/dubbo-kubernetes/app/dubboctl/internal/kube"
"github.com/apache/dubbo-kubernetes/app/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")
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 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
}
}
type DeployConfig struct {
*buildConfig
KubeConfig string
Context string
Build bool
Apply bool
Namespace string
ContainerPort int
Output string
Force bool
TargetPort int
NodePort int
}
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"),
}
return
}