diff --git a/cmd/static/cmd.go b/cmd/static/cmd.go index 95c0a5c9..6d163734 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,6 +120,28 @@ 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.KubeArgument.ParseCli(c); err != nil { + return err + } + return kube.Kube(globalArgs.KubeArgument) + }, + }, { Name: FallbackName, Usage: FallbackUsage, @@ -209,6 +233,21 @@ Examples: Examples: cwgo api --project_path ./ ` + + DockerName = "docker" + DockerUsage = `generate docker file + +Examples: + cwgo docker [args] +` + + KubeName = "kube" + KubeUsage = `generate kube file + +Examples: + cwgo kube [args] +` + JobName = "job" JobUsage = `generate job code diff --git a/cmd/static/docker_flags.go b/cmd/static/docker_flags.go new file mode 100644 index 00000000..d9561e9d --- /dev/null +++ b/cmd/static/docker_flags.go @@ -0,0 +1,67 @@ +package static + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/cloudwego/cwgo/tpl" + "github.com/urfave/cli/v2" +) + +func dockerFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: consts.GoVersion, + Usage: "Specify the go version.", + Aliases: []string{"go"}, + Required: true, + }, + &cli.BoolFlag{ + Name: consts.EnableGoProxy, + Usage: "Enable go proxy.", + DefaultText: "false", + }, + &cli.StringFlag{ + Name: consts.GoProxy, + Usage: "Specify the go proxy.", + DefaultText: "https://goproxy.cn,direct", + }, + &cli.StringFlag{ + Name: consts.Timezone, + Usage: "Specify the timezone.", + DefaultText: "Asia/Shanghai", + }, + &cli.StringFlag{ + Name: consts.BaseImage, + Usage: "Specify the base image.", + DefaultText: "scratch", + }, + &cli.IntFlag{ + Name: consts.Port, + Usage: "Specify the port.", + DefaultText: "0", + }, + &cli.StringFlag{ + Name: consts.GoFileName, + Usage: "Specify the go file name.", + Aliases: []string{"f"}, + Required: true, + }, + &cli.StringFlag{ + Name: consts.ExeFileName, + Usage: "Specify the exe file name.", + }, + &cli.StringFlag{ + Name: consts.Template, + Usage: "Specify the template path. Currently cwgo supports git templates, such as `--template https://github.com/***/cwgo_template.git`", + Aliases: []string{"t"}, + DefaultText: tpl.DockerDir, + }, + &cli.StringFlag{ + Name: consts.Branch, + Usage: "Specify the git template's branch, default is main branch.", + }, + &cli.StringSliceFlag{ + Name: consts.RunArgs, + Usage: "Specify the run args.", + }, + } +} diff --git a/cmd/static/kube_flags.go b/cmd/static/kube_flags.go new file mode 100644 index 00000000..53374a6e --- /dev/null +++ b/cmd/static/kube_flags.go @@ -0,0 +1,69 @@ +package static + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +func kubeFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: consts.Name, + Usage: "Specify the name of the service.", + }, + &cli.StringFlag{ + Name: consts.Namespace, + Usage: "Specify the namespace.", + }, + &cli.StringFlag{ + Name: consts.Image, + Usage: "Specify the image.", + }, + &cli.StringFlag{ + Name: consts.Secret, + Usage: "Specify the secret.", + }, + &cli.IntFlag{ + Name: consts.RequestCpu, + Usage: "Specify the request cpu.", + DefaultText: "500", + }, + &cli.StringFlag{ + Name: consts.RequestMem, + Usage: "Specify the request memory.", + DefaultText: "512", + }, + &cli.StringFlag{ + Name: consts.LimitCpu, + Usage: "Specify the limit cpu.", + DefaultText: "1000", + }, + &cli.StringFlag{ + Name: consts.LimitMem, + Usage: "Specify the limit memory.", + DefaultText: "1024", + }, + &cli.StringFlag{ + Name: consts.Port, + Usage: "Specify the port.", + }, + &cli.StringFlag{ + Name: consts.MinReplicas, + Usage: "Specify the min replicas.", + DefaultText: "3", + }, + &cli.StringFlag{ + Name: consts.MaxReplicas, + Usage: "Specify the max replicas.", + DefaultText: "10", + }, + &cli.StringFlag{ + Name: consts.ImagePullPolicy, + Usage: "Specify the image pull policy. (Always or IfNotPresent or Never)", + }, + &cli.StringFlag{ + Name: consts.ServiceAccount, + Usage: "Specify the service account.", + }, + } +} diff --git a/config/argument.go b/config/argument.go index 68ef14da..9115d0be 100644 --- a/config/argument.go +++ b/config/argument.go @@ -40,6 +40,8 @@ type Argument struct { *DocArgument *JobArgument *ApiArgument + *DockerArgument + *KubeArgument *FallbackArgument } diff --git a/config/docker.go b/config/docker.go new file mode 100644 index 00000000..d6444b95 --- /dev/null +++ b/config/docker.go @@ -0,0 +1,44 @@ +package config + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" + "strings" +) + +type DockerArgument struct { + GoVersion string // compile image go version + EnableGoProxy bool // enable go proxy + GoProxy string // go proxy url + Timezone string // image timezone + BaseImage string // service run image + Port int // docker image expose port + GoFileName string // go project main file name + ExeFileName string // build exe file name + Template string // specify local or remote template path + Branch string // remote template branch + RunArgs string // ext run args +} + +func NewDockerArgument() *DockerArgument { + return &DockerArgument{} +} + +func (c *DockerArgument) ParseCli(ctx *cli.Context) error { + c.GoVersion = ctx.String(consts.GoVersion) + c.EnableGoProxy = ctx.Bool(consts.EnableGoProxy) + c.GoProxy = ctx.String(consts.GoProxy) + c.Timezone = ctx.String(consts.Timezone) + c.BaseImage = ctx.String(consts.BaseImage) + c.Port = ctx.Int(consts.Port) + c.GoFileName = ctx.String(consts.GoFileName) + c.ExeFileName = ctx.String(consts.ExeFileName) + c.Template = ctx.String(consts.Template) + var builder strings.Builder + for _, arg := range ctx.StringSlice(consts.RunArgs) { + builder.WriteString(`, "` + arg + `"`) + } + c.RunArgs = builder.String() + + return nil +} diff --git a/config/kube.go b/config/kube.go new file mode 100644 index 00000000..05db9898 --- /dev/null +++ b/config/kube.go @@ -0,0 +1,41 @@ +package config + +import ( + "github.com/cloudwego/cwgo/pkg/consts" + "github.com/urfave/cli/v2" +) + +type KubeArgument struct { + Name string + Namespace string + Image string + Secret string + RequestCpu int + RequestMem string + LimitCpu string + LimitMem string + MinReplicas int + MaxReplicas int + ImagePullPolicy string + ServiceAccount string +} + +func NewKubeArgument() *KubeArgument { + return &KubeArgument{} +} + +func (c *KubeArgument) ParseCli(ctx *cli.Context) error { + c.Name = ctx.String(consts.Name) + c.Namespace = ctx.String(consts.Namespace) + c.Image = ctx.String(consts.Image) + c.Secret = ctx.String(consts.Secret) + c.RequestCpu = ctx.Int(consts.RequestCpu) + c.RequestMem = ctx.String(consts.RequestMem) + c.LimitCpu = ctx.String(consts.LimitCpu) + c.LimitMem = ctx.String(consts.LimitMem) + c.MinReplicas = ctx.Int(consts.MinReplicas) + c.MaxReplicas = ctx.Int(consts.MaxReplicas) + c.ImagePullPolicy = ctx.String(consts.ImagePullPolicy) + c.ServiceAccount = ctx.String(consts.ServiceAccount) + return nil +} diff --git a/pkg/common/utils/file.go b/pkg/common/utils/file.go index f650c700..5c37f611 100644 --- a/pkg/common/utils/file.go +++ b/pkg/common/utils/file.go @@ -91,3 +91,13 @@ func findRootPathRecursive(currentDirPath, relativeFilePath string) string { return findRootPathRecursive(parentPath, relativeFilePath) } + +// CreateIfNotExist creates a file if it is not exists. +func CreateIfNotExist(file string) (*os.File, error) { + _, err := os.Stat(file) + if !os.IsNotExist(err) { + return nil, fmt.Errorf("%s already exist", file) + } + + return os.Create(file) +} diff --git a/pkg/consts/const.go b/pkg/consts/const.go index f3ba8505..a7be30a5 100644 --- a/pkg/consts/const.go +++ b/pkg/consts/const.go @@ -21,6 +21,9 @@ import "runtime" const ( Kitex = "kitex" Hertz = "hertz" + + Docker = "docker" + Kube = "kube" ) const ( @@ -159,6 +162,33 @@ const ( TypeTag = "type_tag" HexTag = "hex" SQLDir = "sql_dir" + + GoVersion = "go_version" + EnableGoProxy = "enable_go_proxy" + GoProxy = "go_proxy" + Timezone = "timezone" + BaseImage = "base_image" + Port = "port" + GoFileName = "go_file" + ExeFileName = "exe_file" + RunArgs = "run_args" + + Namespace = "namespace" + Image = "image" + Secret = "secret" + RequestCpu = "requestCpu" + RequestMem = "requestMem" + LimitCpu = "limitCpu" + LimitMem = "limitMem" + O = "o" + Replicas = "replicas" + Revisions = "revisions" + NodePort = "nodePort" + TargetPort = "targetPort" + MinReplicas = "minReplicas" + MaxReplicas = "maxReplicas" + ImagePullPolicy = "imagePullPolicy" + ServiceAccount = "serviceAccount" ) const ( diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go new file mode 100644 index 00000000..bc32d0bc --- /dev/null +++ b/pkg/docker/docker.go @@ -0,0 +1,129 @@ +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" + "path/filepath" + "strings" + "text/template" +) + +const ( + dockerfileName = "Dockerfile" + etcDir = "etc" + yamlEtx = ".yaml" +) + +type DockerTemplateParams struct { + GoVersion string + EnableGoProxy bool + GoProxy string + HasTimezone bool + Timezone string + GoRelPath string + GoFileName string + ExeFile string + GoMainFrom string + BaseImage string + Argument string + HasPort bool + Port int +} + +func Docker(c *config.DockerArgument) error { + if strings.HasSuffix(c.Template, consts.SuffixGit) { + // 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 + } else { + isExist, _ := utils.PathExist(path.Join(c.Template, tpl.DockerFileTpl)) + if !isExist { + return errors.New("DockerFile template not exist") + } + } + + if len(c.GoVersion) > 0 { + c.GoVersion = c.GoVersion + "-" + } + + if len(c.GoFileName) > 0 { + isExist, _ := utils.PathExist(c.GoFileName) + if !isExist { + return errors.New("go file not exist") + } + } + + var projectPath string + if len(c.GoFileName) > 0 { + curpath, err := filepath.Abs(consts.CurrentDir) + if err != nil { + return err + } + _, projPath, isOk := utils.SearchGoMod(curpath, true) + if !isOk { + return errors.New("not found go mod") + } + projectPath = projPath + } + + if len(projectPath) == 0 { + projectPath = "." + } + + dockerFile, err := utils.CreateIfNotExist(dockerfileName) + if err != nil { + return err + } + defer dockerFile.Close() + + text, err := utils.ReadFileContent(path.Join(c.Template, tpl.DockerFileTpl)) + if err != nil { + return err + } + + var exeName string + if len(c.ExeFileName) > 0 { + exeName = c.ExeFileName + } else if len(c.GoFileName) > 0 { + exeName = strings.TrimSuffix(filepath.Base(c.GoFileName), filepath.Ext(c.GoFileName)) + } else { + absPath, err := filepath.Abs(projectPath) + if err != nil { + return err + } + + exeName = filepath.Base(absPath) + } + + t := template.Must(template.New("DockerFile").Parse(string(text))) + return t.Execute(dockerFile, DockerTemplateParams{ + GoVersion: c.GoVersion, + EnableGoProxy: c.EnableGoProxy, + GoProxy: c.GoProxy, + HasTimezone: c.Timezone != "", + Timezone: c.Timezone, + GoRelPath: projectPath, + GoFileName: c.GoFileName, + ExeFile: exeName, + GoMainFrom: path.Join(projectPath, c.GoFileName), + BaseImage: c.BaseImage, + Argument: c.RunArgs, + HasPort: c.Port > 0, + Port: c.Port, + }) +} diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go new file mode 100644 index 00000000..a8c5907a --- /dev/null +++ b/pkg/kube/kube.go @@ -0,0 +1,8 @@ +package kube + +import "github.com/cloudwego/cwgo/config" + +func Kube(c *config.KubeArgument) error { + + return nil +} diff --git a/tpl/docker/DockerFile.tpl b/tpl/docker/DockerFile.tpl new file mode 100644 index 00000000..021be6f5 --- /dev/null +++ b/tpl/docker/DockerFile.tpl @@ -0,0 +1,33 @@ +FROM golang:{{.GoVersion}}alpine AS builder + +LABEL stage=gobuilder + +ENV CGO_ENABLED 0 +{{if .EnableGoProxy}}ENV GOPROXY {{.GoProxy}} +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +{{end}}{{if .HasTimezone}} +RUN apk update --no-cache && apk add --no-cache tzdata +{{end}} +WORKDIR /build + +ADD go.mod . +ADD go.sum . +RUN go mod download +COPY . . +COPY {{.GoRelPath}}/etc /app/etc +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 .HasTimezone}}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 .Argument}} +COPY --from=builder /app/etc /app/etc{{end}} +{{if .HasPort}} +EXPOSE {{.Port}} +{{end}} +CMD ["./{{.ExeFile}}"{{.Argument}}] diff --git a/tpl/init.go b/tpl/init.go index 60d5415d..93863330 100644 --- a/tpl/init.go +++ b/tpl/init.go @@ -33,9 +33,22 @@ 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) + + DockerDir = path.Join(os.TempDir(), consts.Docker) + DockerFileTpl = "Dockerfile.tpl" + + KubeDir = path.Join(os.TempDir(), consts.Kube) + KubeDeployTpl = "deployment.tpl" + KubeJobTpl = "job.tpl" ) func Init() { @@ -45,6 +58,8 @@ func Init() { os.Mkdir(HertzDir, 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/deployment.tpl b/tpl/kube/deployment.tpl new file mode 100644 index 00000000..14145df2 --- /dev/null +++ b/tpl/kube/deployment.tpl @@ -0,0 +1,117 @@ +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/v2beta2 +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/v2beta2 +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 diff --git a/tpl/kube/job.tpl b/tpl/kube/job.tpl new file mode 100644 index 00000000..0da72ede --- /dev/null +++ b/tpl/kube/job.tpl @@ -0,0 +1,37 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: {{.Name}} + namespace: {{.Namespace}} +spec: + successfulJobsHistoryLimit: {{.SuccessfulJobsHistoryLimit}} + schedule: "{{.Schedule}}" + jobTemplate: + spec: + template: + spec:{{if .ServiceAccount}} + serviceAccountName: {{.ServiceAccount}}{{end}} + {{end}}containers: + - name: {{.Name}} + image: # todo image url + resources: + requests: + cpu: {{.RequestCpu}}m + memory: {{.RequestMem}}Mi + limits: + cpu: {{.LimitCpu}}m + memory: {{.LimitMem}}Mi + command: + - ./{{.ServiceName}} + - -f + - ./{{.Name}}.yaml + volumeMounts: + - name: timezone + mountPath: /etc/localtime + imagePullSecrets: + - name: # registry secret, if no, remove this + restartPolicy: OnFailure + volumes: + - name: timezone + hostPath: + path: /usr/share/zoneinfo/Asia/Shanghai