diff --git a/.gitignore b/.gitignore index e9fb6818..47c1236e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,3 @@ output/* /platform/manifest/config/config-dev.yaml /platform/manifest/config/config-pro.yaml /platform/dist/ - - diff --git a/cmd/static/cmd.go b/cmd/static/cmd.go index 95c0a5c9..4bb13975 100644 --- a/cmd/static/cmd.go +++ b/cmd/static/cmd.go @@ -23,8 +23,10 @@ import ( "github.com/cloudwego/cwgo/pkg/client" "github.com/cloudwego/cwgo/pkg/consts" "github.com/cloudwego/cwgo/pkg/curd/doc" + "github.com/cloudwego/cwgo/pkg/docker" "github.com/cloudwego/cwgo/pkg/fallback" "github.com/cloudwego/cwgo/pkg/job" + "github.com/cloudwego/cwgo/pkg/kube" "github.com/cloudwego/cwgo/pkg/model" "github.com/cloudwego/cwgo/pkg/server" "github.com/urfave/cli/v2" @@ -118,9 +120,32 @@ func Init() *cli.App { return api_list.Api(globalArgs.ApiArgument) }, }, + { + Name: DockerName, + Usage: DockerUsage, + Flags: dockerFlags(), + Action: func(c *cli.Context) error { + if err := globalArgs.DockerArgument.ParseCli(c); err != nil { + return err + } + return docker.Docker(globalArgs.DockerArgument) + }, + }, + { + Name: KubeName, + Usage: KubeUsage, + Flags: kubeFlags(), + Action: func(c *cli.Context) error { + if err := globalArgs.DockerArgument.ParseCli(c); err != nil { + return err + } + return kube.Kube(globalArgs.KubeArgument) + }, + }, { Name: FallbackName, Usage: FallbackUsage, + Action: func(c *cli.Context) error { if err := globalArgs.FallbackArgument.ParseCli(c); err != nil { return err @@ -229,4 +254,10 @@ Examples: CompletionPowershellName = "powershell" CompletionPowershellUsage = "Generate the autocompletion script for powershell" + + DockerName = "docker" + DockerUsage = "Generate Dockerfile" + + KubeName = "kube" + KubeUsage = "Generate kubernetes files" ) diff --git a/cmd/static/docker_flags.go b/cmd/static/docker_flags.go new file mode 100644 index 00000000..7ae2be9d --- /dev/null +++ b/cmd/static/docker_flags.go @@ -0,0 +1,89 @@ +/* + * Copyright 2022 CloudWeGo Authors + * + * Licensed 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 static + +import ( + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +func dockerFlags() []cli.Flag { + globalArgs := config.GetGlobalArgs() + return []cli.Flag{ + // Required Flags with Default Values + &cli.StringFlag{ + Name: consts.Template, + Usage: "The home path of the template", + Value: consts.Docker, // 使用默认值 consts.Docker + Destination: &globalArgs.DockerArgument.Template, + }, + &cli.StringFlag{ + Name: consts.Exe, + Usage: "The executable name in the built image", + Destination: &globalArgs.DockerArgument.ExeName, + }, + &cli.StringFlag{ + Name: consts.Branch, + Value: consts.MainBranch, // 默认值使用 consts.MainBranch + Usage: "The branch of the remote repo", + Destination: &globalArgs.DockerArgument.Branch, + }, + &cli.StringFlag{ + Name: consts.Base, + Value: consts.Scratch, // 默认值 consts.Scratch + Usage: "The base image to build the docker image, default scratch", + Destination: &globalArgs.DockerArgument.BaseImage, + }, + &cli.StringFlag{ + Name: consts.Go, + Usage: "The file that contains main function", + Value: consts.Main, // 默认值 consts.Main + Destination: &globalArgs.DockerArgument.Main, + }, + &cli.UintFlag{ + Name: consts.Port, + Usage: "The port to expose, default 0 will not expose any port", + Destination: &globalArgs.DockerArgument.Port, + }, + &cli.StringFlag{ + Name: consts.TZ, + Value: consts.AsizShangHai, // 默认值 consts.AsizShangHai + Usage: "The timezone of the container", + Destination: &globalArgs.DockerArgument.TZ, + }, + &cli.StringFlag{ + Name: consts.Version, + Value: consts.Alpine, // 默认值 consts.Alpine + Usage: "The builder golang image version", + Destination: &globalArgs.DockerArgument.Version, + }, + &cli.StringSliceFlag{ + Name: consts.Mirror, + Usage: "The mirror site to use in go update", + }, + &cli.StringSliceFlag{ + Name: consts.Arguments, + Usage: "Arguments will used in go run", + }, + &cli.StringSliceFlag{ + Name: consts.Etc, + Usage: "The etc file dirs path of the project", + Value: cli.NewStringSlice(consts.Etc), // 默认值 consts.Etc + }, + } +} diff --git a/cmd/static/kube_flags.go b/cmd/static/kube_flags.go new file mode 100644 index 00000000..28a99c24 --- /dev/null +++ b/cmd/static/kube_flags.go @@ -0,0 +1,132 @@ +package static + +import ( + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +func kubeFlags() []cli.Flag { + globalArgs := config.GetGlobalArgs() + return []cli.Flag{ + // Required Flags + &cli.StringFlag{ + Name: consts.Name, // Use consts.Name constant + Required: true, + Usage: "The name of the deployment", + Destination: &globalArgs.KubeArgument.Name, + }, + &cli.StringFlag{ + Name: consts.Namespace, // Use consts.Namespace constant + Required: true, + Usage: "The namespace of the deployment", + Destination: &globalArgs.KubeArgument.Namespace, + }, + &cli.StringFlag{ + Name: consts.Output, // Use consts.Output constant + Aliases: []string{"o"}, // Alias "o" for "output" + Required: true, + Usage: "The output yaml file", + Destination: &globalArgs.KubeArgument.Output, + }, + &cli.IntFlag{ + Name: consts.Port, // Use consts.Port constant + Required: true, + Usage: "The port of the deployment to listen on pod", + Destination: &globalArgs.KubeArgument.Port, + }, + + // Optional Flags with Default Values + &cli.StringFlag{ + Name: consts.Branch, // Use consts.Branch constant + Usage: "The branch of the remote repo, works with --remote", + Destination: &globalArgs.KubeArgument.Branch, + }, + &cli.StringFlag{ + Name: consts.Template, // Use consts.Template constant + Value: consts.Kube, // Use consts.Kube constant + Usage: "The home path of the template", + Destination: &globalArgs.KubeArgument.Template, + }, + &cli.StringFlag{ + Name: consts.Image, // Use consts.Image constant + Required: true, + Usage: "The docker image for the deployment (required)", + Destination: &globalArgs.KubeArgument.Image, + }, + &cli.StringFlag{ + Name: consts.ImagePullPolicy, // Use consts.ImagePullPolicy constant + Usage: "Image pull policy. One of Always, Never, IfNotPresent", + Destination: &globalArgs.KubeArgument.ImagePullPolicy, + }, + &cli.IntFlag{ + Name: consts.LimitCpu, // Use consts.LimitCpu constant + Usage: "The CPU limit for deployment (default 1000)", + Value: consts.DefaultLimitCpu, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.LimitCpu, + }, + &cli.IntFlag{ + Name: consts.LimitMem, // Use consts.LimitMem constant + Usage: "The memory limit for deployment (default 1024)", + Value: consts.DefaultLimitMem, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.LimitMem, + }, + &cli.IntFlag{ + Name: consts.MaxReplicas, // Use consts.MaxReplicas constant + Usage: "The maximum number of replicas for deployment (default 10)", + Value: consts.DefaultMaxReplicas, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.MaxReplicas, + }, + &cli.IntFlag{ + Name: consts.MinReplicas, // Use consts.MinReplicas constant + Usage: "The minimum number of replicas for deployment (default 3)", + Value: consts.DefaultMinReplicas, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.MinReplicas, + }, + + &cli.IntFlag{ + Name: consts.Replicas, // Use consts.Replicas constant + Usage: "The number of replicas for deployment (default 3)", + Value: consts.DefaultReplicas, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.Replicas, + }, + &cli.IntFlag{ + Name: consts.RequestCpu, // Use consts.RequestCpu constant + Usage: "The requested CPU for deployment (default 500)", + Value: consts.DefaultRequestCpu, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.RequestCpu, + }, + &cli.IntFlag{ + Name: consts.RequestMem, // Use consts.RequestMem constant + Usage: "The requested memory for deployment (default 512)", + Value: consts.DefaultRequestMem, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.RequestMem, + }, + &cli.IntFlag{ + Name: consts.Revisions, // Use consts.Revisions constant + Usage: "The number of revision histories to limit (default 5)", + Value: consts.DefaultRevisions, // Use consts.DefaultLimitCpu constant + Destination: &globalArgs.KubeArgument.Revisions, + }, + &cli.StringFlag{ + Name: consts.Secret, // Use consts.Secret constant + Usage: "The secret to pull the image from the registry", + Destination: &globalArgs.KubeArgument.Secret, + }, + &cli.StringFlag{ + Name: consts.ServiceAccount, // Use consts.ServiceAccount constant + Usage: "The ServiceAccount for the deployment", + Destination: &globalArgs.KubeArgument.ServiceAccount, + }, + &cli.IntFlag{ + Name: consts.NodePort, // Use consts.NodePort constant + Usage: "The nodePort for the deployment to expose", + Destination: &globalArgs.KubeArgument.NodePort, + }, + &cli.IntFlag{ + Name: consts.TargetPort, // Use consts.TargetPort constant + Usage: "The targetPort for the deployment, default to port", + Destination: &globalArgs.KubeArgument.TargetPort, + }, + } +} diff --git a/config/argument.go b/config/argument.go index 68ef14da..0186bdde 100644 --- a/config/argument.go +++ b/config/argument.go @@ -41,6 +41,8 @@ type Argument struct { *JobArgument *ApiArgument *FallbackArgument + *DockerArgument + *KubeArgument } func NewArgument() *Argument { @@ -50,8 +52,10 @@ func NewArgument() *Argument { ModelArgument: NewModelArgument(), DocArgument: NewDocArgument(), JobArgument: NewJobArgument(), + DockerArgument: NewDockerArgument(), ApiArgument: NewApiArgument(), FallbackArgument: NewFallbackArgument(), + KubeArgument: NewKubeArgument(), } } diff --git a/config/docker.go b/config/docker.go new file mode 100644 index 00000000..38351e1b --- /dev/null +++ b/config/docker.go @@ -0,0 +1,39 @@ +package config + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +type DockerArgument struct { + BaseImage string + Branch string + ExeName string + Main string + Template string + Port uint + TZ string + Version string + Mirrors []string + Arguments []string + EtcDirs []string +} + +func NewDockerArgument() *DockerArgument { + return &DockerArgument{} +} + +func (s *DockerArgument) ParseCli(ctx *cli.Context) error { + s.Template = ctx.String(consts.Template) + s.BaseImage = ctx.String(consts.Base) + s.ExeName = ctx.String(consts.Exe) + s.Branch = ctx.String(consts.Branch) + s.Main = ctx.String(consts.Go) + s.Port = ctx.Uint(consts.Port) + s.TZ = ctx.String(consts.TZ) + s.Version = ctx.String(consts.Version) + s.Mirrors = ctx.StringSlice(consts.Mirror) + s.Arguments = ctx.StringSlice(consts.Arguments) + s.EtcDirs = ctx.StringSlice(consts.Etc) + return nil +} diff --git a/config/k8s.go b/config/k8s.go new file mode 100644 index 00000000..6e5f8fff --- /dev/null +++ b/config/k8s.go @@ -0,0 +1,59 @@ +package config + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +type KubeArgument struct { + Template string + Name string + Namespace string + Port int + Image string + ImagePullPolicy string + LimitCpu int + LimitMem int + MaxReplicas int + MinReplicas int + Output string + Replicas int + Branch string + RequestCpu int + RequestMem int + Revisions int + Secret string + ServiceAccount string + NodePort int + TargetPort int +} + +func NewKubeArgument() *KubeArgument { + return &KubeArgument{} +} + +func (s *KubeArgument) ParseCli(ctx *cli.Context) error { + // Parse all the arguments from the CLI context + s.Name = ctx.String(consts.Name) + s.Namespace = ctx.String(consts.Namespace) + s.Port = ctx.Int(consts.Port) + s.Image = ctx.String(consts.Image) + s.ImagePullPolicy = ctx.String(consts.ImagePullPolicy) + s.LimitCpu = ctx.Int(consts.LimitCpu) + s.LimitMem = ctx.Int(consts.LimitMem) + s.MaxReplicas = ctx.Int(consts.MaxReplicas) + s.MinReplicas = ctx.Int(consts.MinReplicas) + s.Output = ctx.String(consts.Output) + s.Replicas = ctx.Int(consts.Replicas) + s.Branch = ctx.String(consts.Branch) + s.Template = ctx.String(consts.Template) + s.RequestCpu = ctx.Int(consts.RequestCpu) + s.RequestMem = ctx.Int(consts.RequestMem) + s.Revisions = ctx.Int(consts.Revisions) + s.Secret = ctx.String(consts.Secret) + s.ServiceAccount = ctx.String(consts.ServiceAccount) + s.NodePort = ctx.Int(consts.NodePort) + s.TargetPort = ctx.Int(consts.TargetPort) + + return nil +} diff --git a/pkg/common/utils/file.go b/pkg/common/utils/file.go index f650c700..97091b78 100644 --- a/pkg/common/utils/file.go +++ b/pkg/common/utils/file.go @@ -33,11 +33,34 @@ func PathExist(path string) (bool, error) { } _, err = os.Stat(abPath) if err != nil { - return os.IsExist(err), nil + // Check if the error is due to the path not existing + if os.IsNotExist(err) { + return false, nil + } + // For other errors (e.g., permission issues), return them + return false, err } + // Path exists return true, nil } +// PathExist is used to find all file's in the path. +func GetAllFile(pathname string, s []string) ([]string, error) { + rd, err := os.ReadDir(pathname) + if err != nil { + fmt.Println("read dir fail:", err) + return s, err + } + + for _, fi := range rd { + if !fi.IsDir() { + fullName := pathname + "/" + fi.Name() + s = append(s, fullName) + } + } + return s, nil +} + // GetIdlType is used to return the idl type. func GetIdlType(path string, pbName ...string) (string, error) { ext := filepath.Ext(path) @@ -68,6 +91,16 @@ func ReadFileContent(filePath string) (content []byte, err error) { return io.ReadAll(file) } +func WriteFile(path string) (wr *os.File, err error) { + _, err = os.Stat(path) + if os.IsNotExist(err) { + return os.Create(path) + } else { + return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + } + +} + func CreateFile(path, content string) (err error) { return os.WriteFile(path, []byte(content), os.FileMode(0o644)) } diff --git a/pkg/consts/const.go b/pkg/consts/const.go index f3ba8505..abc1a3e3 100644 --- a/pkg/consts/const.go +++ b/pkg/consts/const.go @@ -16,11 +16,14 @@ package consts -import "runtime" +import ( + "runtime" +) const ( - Kitex = "kitex" - Hertz = "hertz" + Kitex = "kitex" + Hertz = "hertz" + Docker = "docker" ) const ( @@ -61,6 +64,7 @@ const ( Comma = ";" Tilde = "~" LineBreak = "\n" + Dash = "-" ) // Package Name @@ -169,6 +173,59 @@ const ( JobName = "job_name" ) +const ( + Base = "base" + Exe = "exe" + Port = "port" + TZ = "tz" + Version = "version" + + Scratch = "scratch" + AsizShangHai = "Asia/Shanghai" + MainBranch = "main" + Mirror = "mirror" + + HttpsPrefix = "https://" + MirrorSep = "," + MirrorDirect = "direct" + Arguments = "arguments" + Etc = "etc" + Dockerfile = "Dockerfile" + Alpine = "alpine" + DefaultDockerfileTpl = "dockerfile_tpl.yaml" + Yaml = ".yaml" +) + +const ( + Kube = "kube" + Namespace = "namespace" + Image = "image" + ImagePullPolicy = "imagePullPolicy" + LimitCpu = "limitCpu" + LimitMem = "limitMem" + MaxReplicas = "maxReplicas" + MinReplicas = "minReplicas" + Output = "output" + Replicas = "replicas" + RequestCpu = "requestCpu" + RequestMem = "requestMem" + Revisions = "revisions" + Secret = "secret" + ServiceAccount = "serviceAccount" + NodePort = "nodePort" + TargetPort = "targetPort" + DefaultKubeDeployTpl = "deployment_tpl.yaml" + + DefaultLimitCpu = 1000 + DefaultLimitMem = 1024 + DefaultMaxReplicas = 10 + DefaultMinReplicas = 3 + DefaultReplicas = 3 + DefaultRequestCpu = 500 + DefaultRequestMem = 512 + DefaultRevisions = 512 +) + const ( BashAutocomplete = `#! /bin/bash diff --git a/pkg/curd/doc/doc.go b/pkg/curd/doc/doc.go index 723035c0..135387a4 100644 --- a/pkg/curd/doc/doc.go +++ b/pkg/curd/doc/doc.go @@ -30,7 +30,6 @@ import ( "github.com/cloudwego/cwgo/config" "github.com/cloudwego/cwgo/pkg/consts" - "github.com/cloudwego/hertz/cmd/hz/util/logs" ) func Doc(c *config.DocArgument) error { @@ -40,7 +39,7 @@ func Doc(c *config.DocArgument) error { switch c.Name { case consts.MongoDb: - setLogVerbose(c.Verbose) + utils.SetHzVerboseLog(c.Verbose) if err := plugin.MongoTriggerPlugin(c); err != nil { return err } @@ -191,11 +190,3 @@ func check(c *config.DocArgument) (err error) { return nil } - -func setLogVerbose(verbose bool) { - if verbose { - logs.SetLevel(logs.LevelDebug) - } else { - logs.SetLevel(logs.LevelWarn) - } -} diff --git a/pkg/docker/check.go b/pkg/docker/check.go new file mode 100644 index 00000000..c0a06bf4 --- /dev/null +++ b/pkg/docker/check.go @@ -0,0 +1,55 @@ +package docker + +import ( + "errors" + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/common/utils" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/cloudwego/cwgo/tpl" + "path/filepath" + "strings" +) + +func check(c *config.DockerArgument) error { + if c.Template == consts.Docker { + c.Template = tpl.DockerDir + } + if c.Main == "" { + return errors.New("go main file must be provided") + } + + if len(c.Version) > 0 { + c.Version = c.Version + consts.Dash + } + + if len(c.Main) > 0 { + isExist, _ := utils.PathExist(c.Main) + if !isExist { + return errors.New("go main not exist") + } + } + + curPath, err := filepath.Abs(consts.CurrentDir) + if err != nil { + return err + } + _, _, isOk := utils.SearchGoMod(curPath, true) + if !isOk { + return errors.New("not found go mod") + } + + if c.Port > 0 && c.Port < 1024 || c.Port < 0 || c.Port > 65535 { + return errors.New("port must between 1024 and 65535") + } + + if !strings.HasSuffix(c.Template, consts.SuffixGit) { + isExist, err := utils.PathExist(c.Template) + if err != nil { + return err + } + if !isExist { + return errors.New("DockerFile template not exist") + } + } + return nil +} diff --git a/pkg/docker/description.go b/pkg/docker/description.go new file mode 100644 index 00000000..f07a66a1 --- /dev/null +++ b/pkg/docker/description.go @@ -0,0 +1,102 @@ +package docker + +import ( + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/common/utils" + "github.com/cloudwego/cwgo/pkg/consts" + "strings" +) + +// DockerfileInfo contains information used to generate a Dockerfile for a template. +type DockerfileInfo struct { + Mirrors string // Mirrors holds the mirror URLs + GoMainFrom string // GoMainFrom is the path to the Go main file + GoFile string // GoFile is the Go file Path (main file) + ExeFile string // ExeFile is the Path of the executable file + BaseImage string // BaseImage is the base image for the Dockerfile + Port int // Port is the port number + EtcDirs []string // EtcDirs contains the directory containing the etc files + Argument string // Argument contains the list of build arguments for the Dockerfile + Version string // Version is the version number of the application + Timezone string // Timezone is the timezone for the application +} + +// FillInfo populates a DockerfileInfo struct with data from the DockerArgument configuration. +func FillInfo(c *config.DockerArgument) *DockerfileInfo { + // Initialize mirror URL builder + var mirrorBuilder strings.Builder + // Check if mirrors are provided and construct the mirror string + if len(c.Mirrors) > 0 { + for _, mirror := range c.Mirrors { + // Ensure each mirror URL starts with the correct prefix + if !strings.HasPrefix(mirror, consts.HttpsPrefix) { + mirrorBuilder.WriteString(consts.HttpsPrefix) + } + mirrorBuilder.WriteString(mirror) + mirrorBuilder.WriteString(consts.MirrorSep) + } + // Append "direct" at the end of the mirror list + mirrorBuilder.WriteString(consts.MirrorDirect) + } + + // Initialize arguments slice for the Dockerfile + var args []string + + // Check if there are any etc files to include + var hasEtcFile, etcPaths = checkEtcPath(c.EtcDirs) + if hasEtcFile { + args = append(args, "-f") + for _, path := range etcPaths { + var err error + args, err = utils.GetAllFile(path, args) + if err != nil { + return nil + } + } + } + + // Append any additional arguments provided in the DockerArgument + args = append(args, c.Arguments...) + var argsStr string + for _, str := range args { + if !strings.HasPrefix(str, "\"") { + str = "\"" + str + } + if !strings.HasSuffix(str, "\"") { + str = str + "\"" // Wrap each argument in double quotes for Dockerfile compatibility + } + argsStr = argsStr + ", " + str // Separate arguments with spaces for Dockerfile compatibility + } + + // Return a populated DockerfileInfo struct + return &DockerfileInfo{ + Mirrors: mirrorBuilder.String(), // Constructed mirror URLs + GoMainFrom: c.Main, // Path to the main Go file + GoFile: c.Main, // Main Go file Path + ExeFile: c.ExeName, // Executable file Path + BaseImage: c.BaseImage, // Base Docker image + Port: int(c.Port), // Port number + EtcDirs: etcPaths, // Etc directory containing + Argument: argsStr, // List of Docker build arguments + Version: c.Version, // Version number of the application + Timezone: c.TZ, // Timezone setting + } +} + +// checkEtcPath checks if there are any files in the "etc" directory. +func checkEtcPath(paths []string) (bool, []string) { + etdPaths := make([]string, 0) + for _, dir := range paths { + // 检查路径是否存在 + if exists, err := utils.PathExist(dir); err != nil { + return false, nil // 直接返回,如果路径不存在或发生错误 + } else if !exists { + continue + } + + // 路径有效,添加到 etdPaths 列表 + etdPaths = append(etdPaths, dir) + } + + return len(etdPaths) > 0, etdPaths +} diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go new file mode 100644 index 00000000..50dce227 --- /dev/null +++ b/pkg/docker/docker.go @@ -0,0 +1,69 @@ +package docker + +import ( + "fmt" + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/common/utils" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/cloudwego/cwgo/tpl" + "path" + "path/filepath" + "strings" + "text/template" +) + +func pullGitTpl(c *config.DockerArgument) error { + // pull remote template + err := utils.GitClone(c.Template, tpl.DockerDir) + if err != nil { + return err + } + gitPath, err := utils.GitPath(c.Template) + if err != nil { + return err + } + gitPath = path.Join(tpl.DockerDir, gitPath) + if err = utils.GitCheckout(c.Branch, gitPath); err != nil { + return err + } + c.Template = gitPath + return nil +} + +func Docker(c *config.DockerArgument) error { + if err := check(c); err != nil { + return err + } + + dockerfileInfo := FillInfo(c) + + var tplFile = new(DockerfilesTpl) + var url string + if strings.HasSuffix(c.Template, consts.SuffixGit) { + if err := pullGitTpl(c); err != nil { + return err + } + url = path.Join(c.Template, consts.DefaultDockerfileTpl) + } else if strings.HasSuffix(c.Template, consts.Yaml) { + url = c.Template + } else { + url = path.Join(c.Template, path.Join(consts.Standard, consts.DefaultDockerfileTpl)) + } + if err := tplFile.FromYAMLFile(url); err != nil { + return err + } + t := template.Must(template.New(consts.Dockerfile).Parse(tplFile.Body)) + + wr, err := utils.WriteFile(tplFile.Path) + if err != nil { + return err + } + defer wr.Close() + + if err := t.Execute(wr, dockerfileInfo); err != nil { + return err + } + var str, _ = filepath.Abs(consts.CurrentDir) + fmt.Print("Hint: run \"docker build ...\" command in dir:\n" + "\t" + str + "\nDone.") + return nil +} diff --git a/pkg/docker/template.go b/pkg/docker/template.go new file mode 100644 index 00000000..a3d2824f --- /dev/null +++ b/pkg/docker/template.go @@ -0,0 +1,23 @@ +package docker + +import ( + "gopkg.in/yaml.v3" + "os" +) + +type DockerfilesTpl struct { + Body string `json:"body"` + Path string `json:"path"` +} + +// FromYAMLFile unmarshals a DockerfilesTpl with YAML format from the given file. +func (p *DockerfilesTpl) FromYAMLFile(filename string) error { + if p == nil { + return nil + } + data, err := os.ReadFile(filename) + if err != nil { + return err + } + return yaml.Unmarshal(data, p) +} diff --git a/pkg/kube/check.go b/pkg/kube/check.go new file mode 100644 index 00000000..befc331b --- /dev/null +++ b/pkg/kube/check.go @@ -0,0 +1,115 @@ +package kube + +import ( + "errors" + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/common/utils" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/cloudwego/cwgo/tpl" + "strings" +) + +// check validates the configuration arguments for Kubernetes deployment. +func check(c *config.KubeArgument) error { + // Validate template type; set default if necessary. + if c.Template == consts.Kube { + c.Template = tpl.KubeDir + } + + // Validate port number is within the valid range (1024-65535). + if c.Port > 0 && (c.Port < 1024 || c.Port > 65535) { + return errors.New("port must be between 1024 and 65535") + } + + // Validate image name is provided. + if c.Image == "" { + return errors.New("image must be provided") + } + + // Validate image pull policy (valid values: Always, Never, IfNotPresent). + if c.ImagePullPolicy != "" && !isValidImagePullPolicy(c.ImagePullPolicy) { + return errors.New("invalid imagePullPolicy. Valid values are Always, Never, IfNotPresent") + } + + // Validate CPU and memory limits and requests; set defaults if not provided. + if c.LimitCpu <= 0 { + c.LimitCpu = 1000 + } + if c.LimitMem <= 0 { + c.LimitMem = 1024 + } + if c.RequestCpu <= 0 { + c.RequestCpu = 500 + } + if c.RequestMem <= 0 { + c.RequestMem = 512 + } + + // Validate replica count; set defaults if not provided. + if c.Replicas <= 0 { + c.Replicas = 3 + } + if c.MaxReplicas <= 0 { + c.MaxReplicas = 10 + } + if c.MinReplicas <= 0 { + c.MinReplicas = 3 + } + // Validate that MinReplicas is less than or equal to MaxReplicas. + if c.MinReplicas > c.MaxReplicas { + return errors.New("minReplicas must be less than or equal to maxReplicas") + } + + // Validate deployment name is provided. + if c.Name == "" { + return errors.New("name must be provided") + } + + // Validate Kubernetes namespace is provided. + if c.Namespace == "" { + return errors.New("namespace must be provided") + } + + // Validate nodePort is within the valid range (30000-32767). + if c.NodePort > 0 && (c.NodePort < 30000 || c.NodePort > 32767) { + return errors.New("nodePort must be between 30000 and 32767") + } + + // Validate targetPort; set it to the value of port if not provided. + if c.TargetPort <= 0 { + c.TargetPort = c.Port + } + + // Validate output YAML file name is provided. + if c.Output == "" { + return errors.New("output file (o) must be provided") + } + + // Validate secret field is provided if specified. + if len(c.Secret) > 0 && c.Secret == "" { + return errors.New("secret must be provided if specified") + } + + // Validate service account field is provided if specified. + if len(c.ServiceAccount) > 0 && c.ServiceAccount == "" { + return errors.New("serviceAccount must be provided if specified") + } + + // Validate template path exists. + if !strings.HasSuffix(c.Template, consts.SuffixGit) { + isExist, err := utils.PathExist(c.Template) + if err != nil { + return err + } + if !isExist { + return errors.New("DockerFile template not exist") + } + } + + return nil +} + +// isValidImagePullPolicy checks if the provided image pull policy is valid. +func isValidImagePullPolicy(policy string) bool { + return policy == "Always" || policy == "Never" || policy == "IfNotPresent" +} diff --git a/pkg/kube/description.go b/pkg/kube/description.go new file mode 100644 index 00000000..b69bf835 --- /dev/null +++ b/pkg/kube/description.go @@ -0,0 +1,49 @@ +package kube + +import "github.com/cloudwego/cwgo/config" + +// KubeInfo contains all the information required to generate a Kubernetes deployment. +type KubeInfo struct { + Name string + Namespace string + Image string + Secret string + Replicas int + Revisions int + Port int + TargetPort int + NodePort int + UseNodePort bool + RequestCpu int + RequestMem int + LimitCpu int + LimitMem int + MinReplicas int + MaxReplicas int + ServiceAccount string + ImagePullPolicy string +} + +// FillInfo populates a KubeInfo struct with data from the KubeArgument configuration. +func FillInfo(c *config.KubeArgument) *KubeInfo { + return &KubeInfo{ + Name: c.Name, + Namespace: c.Namespace, + Image: c.Image, + Secret: c.Secret, + Replicas: c.Replicas, + Revisions: c.Revisions, + Port: c.Port, + TargetPort: c.TargetPort, + NodePort: c.NodePort, + UseNodePort: c.NodePort > 0, // If NodePort is greater than 0, UseNodePort is true + RequestCpu: c.RequestCpu, + RequestMem: c.RequestMem, + LimitCpu: c.LimitCpu, + LimitMem: c.LimitMem, + MinReplicas: c.MinReplicas, + MaxReplicas: c.MaxReplicas, + ServiceAccount: c.ServiceAccount, + ImagePullPolicy: c.ImagePullPolicy, + } +} diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go new file mode 100644 index 00000000..77ae82b5 --- /dev/null +++ b/pkg/kube/kube.go @@ -0,0 +1,73 @@ +package kube + +import ( + "fmt" + "github.com/cloudwego/cwgo/config" + "github.com/cloudwego/cwgo/pkg/common/utils" + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/cloudwego/cwgo/tpl" + "path" + "path/filepath" + "strings" + "text/template" +) + +func pullGitTpl(c *config.KubeArgument) error { + // pull remote template + err := utils.GitClone(c.Template, tpl.DockerDir) + if err != nil { + return err + } + gitPath, err := utils.GitPath(c.Template) + if err != nil { + return err + } + gitPath = path.Join(tpl.DockerDir, gitPath) + if err = utils.GitCheckout(c.Branch, gitPath); err != nil { + return err + } + c.Template = gitPath + return nil +} + +func Kube(c *config.KubeArgument) error { + if err := check(c); err != nil { + return err + } + + dockerfileInfo := FillInfo(c) + + var tplFile = new(KubeDeployTpl) + var url string + if strings.HasSuffix(c.Template, consts.SuffixGit) { + if err := pullGitTpl(c); err != nil { + return err + } + url = path.Join(c.Template, consts.DefaultDockerfileTpl) + } else if strings.HasSuffix(c.Template, consts.Yaml) { + url = c.Template + } else { + url = path.Join(c.Template, path.Join(consts.Standard, consts.DefaultKubeDeployTpl)) + } + if err := tplFile.FromYAMLFile(url); err != nil { + return err + } + t := template.Must(template.New(consts.Dockerfile).Parse(tplFile.Body)) + + wr, err := utils.WriteFile(c.Output) + if err != nil { + return err + } + defer wr.Close() + + if err := t.Execute(wr, dockerfileInfo); err != nil { + return err + } + abs, err := filepath.Abs(filepath.Join(consts.CurrentDir, c.Output)) + if err != nil { + return err + } + fmt.Println("Output to:", abs) + fmt.Println("Done.") + return nil +} diff --git a/pkg/kube/template.go b/pkg/kube/template.go new file mode 100644 index 00000000..1d2462c0 --- /dev/null +++ b/pkg/kube/template.go @@ -0,0 +1,23 @@ +package kube + +import ( + "gopkg.in/yaml.v3" + "os" +) + +type KubeDeployTpl struct { + Body string `json:"body"` + Path string `json:"path"` +} + +// FromYAMLFile unmarshals a KubeDeployTpl with YAML format from the given file. +func (p *KubeDeployTpl) FromYAMLFile(filename string) error { + if p == nil { + return nil + } + data, err := os.ReadFile(filename) + if err != nil { + return err + } + return yaml.Unmarshal(data, p) +} diff --git a/tpl/docker/standard/dockerfile_tpl.yaml b/tpl/docker/standard/dockerfile_tpl.yaml new file mode 100644 index 00000000..74e2e531 --- /dev/null +++ b/tpl/docker/standard/dockerfile_tpl.yaml @@ -0,0 +1,42 @@ +path: Dockerfile +body: |- + FROM golang:{{.Version}}alpine AS builder + + LABEL stage=gobuilder + + ENV CGO_ENABLED=0 + + {{if .Mirrors}}ENV GOPROXY=https://goproxy.cn,direct + RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories + {{end}}{{if .Timezone}} + RUN apk update --no-cache && apk add --no-cache tzdata + {{end}} + WORKDIR /build + + ADD go.mod . + ADD go.sum . + RUN go mod download + + COPY . . + {{if .EtcDirs}}{{range .EtcDirs}}COPY {{.}} /app/{{.}} + {{end}}{{end}} + + RUN go build -ldflags="-s -w" -o /app/{{.ExeFile}} {{.GoMainFrom}} + + + FROM {{.BaseImage}} + + COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + {{if .Timezone}}COPY --from=builder /usr/share/zoneinfo/{{.Timezone}} /usr/share/zoneinfo/{{.Timezone}} + ENV TZ={{.Timezone}} + {{end}} + + WORKDIR /app + COPY --from=builder /app/{{.ExeFile}} /app/{{.ExeFile}} + {{if .EtcDirs}}{{range .EtcDirs}}COPY --from=builder /app/{{.}} /app/{{.}} + {{end}}{{end}} + + {{if .Port}}EXPOSE {{.Port}} + {{end}} + + CMD ["./{{.ExeFile}}"{{.Argument}}] diff --git a/tpl/init.go b/tpl/init.go index 60d5415d..96a5b48f 100644 --- a/tpl/init.go +++ b/tpl/init.go @@ -33,18 +33,32 @@ var kitexTpl embed.FS //go:embed hertz var hertzTpl embed.FS +//go:embed docker +var dockerTpl embed.FS + +//go:embed kube +var kubeTpl embed.FS + var ( - KitexDir = path.Join(os.TempDir(), consts.Kitex) - HertzDir = path.Join(os.TempDir(), consts.Hertz) + KitexDir = path.Join(os.TempDir(), consts.Kitex) + HertzDir = path.Join(os.TempDir(), consts.Hertz) + DockerDir = path.Join(os.TempDir(), consts.Docker) + KubeDir = path.Join(os.TempDir(), consts.Kube) ) func Init() { os.RemoveAll(KitexDir) os.RemoveAll(HertzDir) + os.RemoveAll(DockerDir) + os.RemoveAll(KubeDir) os.Mkdir(KitexDir, 0o755) os.Mkdir(HertzDir, 0o755) + os.Mkdir(DockerDir, 0o755) + os.Mkdir(KubeDir, 0o755) initDir(kitexTpl, consts.Kitex, KitexDir) initDir(hertzTpl, consts.Hertz, HertzDir) + initDir(dockerTpl, consts.Docker, DockerDir) + initDir(kubeTpl, consts.Kube, KubeDir) } func initDir(fs embed.FS, srcDir, dstDir string) { diff --git a/tpl/kube/standard/deployment_tpl.yaml b/tpl/kube/standard/deployment_tpl.yaml new file mode 100644 index 00000000..fde24790 --- /dev/null +++ b/tpl/kube/standard/deployment_tpl.yaml @@ -0,0 +1,120 @@ +path: deployment +body: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: {{.Name}} + namespace: {{.Namespace}} + labels: + app: {{.Name}} + spec: + replicas: {{.Replicas}} + revisionHistoryLimit: {{.Revisions}} + selector: + matchLabels: + app: {{.Name}} + template: + metadata: + labels: + app: {{.Name}} + spec:{{if .ServiceAccount}} + serviceAccountName: {{.ServiceAccount}}{{end}} + containers: + - name: {{.Name}} + image: {{.Image}} + {{if .ImagePullPolicy}}imagePullPolicy: {{.ImagePullPolicy}} + {{end}}ports: + - containerPort: {{.Port}} + readinessProbe: + tcpSocket: + port: {{.Port}} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: {{.Port}} + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + requests: + cpu: {{.RequestCpu}}m + memory: {{.RequestMem}}Mi + limits: + cpu: {{.LimitCpu}}m + memory: {{.LimitMem}}Mi + volumeMounts: + - name: timezone + mountPath: /etc/localtime + {{if .Secret}}imagePullSecrets: + - name: {{.Secret}} + {{end}}volumes: + - name: timezone + hostPath: + path: /usr/share/zoneinfo/Asia/Shanghai + + --- + + apiVersion: v1 + kind: Service + metadata: + name: {{.Name}}-svc + namespace: {{.Namespace}} + spec: + ports: + {{if .UseNodePort}}- nodePort: {{.NodePort}} + port: {{.Port}} + protocol: TCP + targetPort: {{.TargetPort}} + type: + NodePort{{else}}- port: {{.Port}} + targetPort: {{.TargetPort}}{{end}} + selector: + app: {{.Name}} + + --- + + apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: {{.Name}}-hpa-c + namespace: {{.Namespace}} + labels: + app: {{.Name}}-hpa-c + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{.Name}} + minReplicas: {{.MinReplicas}} + maxReplicas: {{.MaxReplicas}} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 80 + + --- + + apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: {{.Name}}-hpa-m + namespace: {{.Namespace}} + labels: + app: {{.Name}}-hpa-m + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{.Name}} + minReplicas: {{.MinReplicas}} + maxReplicas: {{.MaxReplicas}} + metrics: + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80