diff --git a/README.md b/README.md index 1a43bec..50df2ab 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ hd install -t 8 linuxsuren/ks/kubectl-ks All features below work with [KubeSphere](https://github.com/kubsphere/kubesphere) instead of other concept. * Pipeline management + * Create a Pipeline with java, go template * Edit a Pipeline without give the fullname (namespace/name) * User Management * Component Management @@ -45,6 +46,7 @@ Aliases: pipeline, pip Available Commands: + create Create a Pipeline in the KubeSphere cluster delete Delete a specific Pipeline of KubeSphere DevOps edit Edit the target pipeline view Output the YAML format of a Pipeline diff --git a/go.mod b/go.mod index a2612c2..6047c60 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,14 @@ go 1.15 require ( github.com/AlecAivazis/survey/v2 v2.2.7 + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible + github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.9 // indirect github.com/linuxsuren/cobra-extension v0.0.10 github.com/linuxsuren/go-cli-alias v0.0.4 + github.com/mitchellh/copystructure v1.1.1 // indirect github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 304fe15..0f46787 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,12 @@ github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6L github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= @@ -167,6 +173,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -208,6 +215,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= @@ -283,6 +292,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.1.1 h1:Bp6x9R1Wn16SIz3OfeDr0b7RnCG2OB66Y7PQyC/cvq4= +github.com/mitchellh/copystructure v1.1.1/go.mod h1:EBArHfARyrSWO/+Wyr9zwEkc6XMFB9XyNgFNmRkZZU4= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -291,6 +302,8 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= +github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/kubectl-plugin/pipeline/create.go b/kubectl-plugin/pipeline/create.go new file mode 100644 index 0000000..d604f49 --- /dev/null +++ b/kubectl-plugin/pipeline/create.go @@ -0,0 +1,318 @@ +package pipeline + +import ( + "bytes" + "context" + "fmt" + "github.com/Masterminds/sprig" + "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + "html" + "html/template" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/dynamic" + "strings" +) + +type pipelineCreateOption struct { + Workspace string + Project string + Name string + Jenkinsfile string + Template string + + // Inner fields + Client dynamic.Interface + WorkspaceUID string +} + +func newPipelineCreateCmd(client dynamic.Interface) (cmd *cobra.Command) { + opt := &pipelineCreateOption{ + Client: client, + } + + cmd = &cobra.Command{ + Use: "create", + Short: "Create a Pipeline in the KubeSphere cluster", + Long: `Create a Pipeline in the KubeSphere cluster +You can create a Pipeline with a java, go template. Before you do that, please make sure the workspace exists. +KubeSphere supports multiple types Pipeline. Currently, this CLI only support the simple one with Jenkinsfile inside.'`, + Example: "ks pip create --ws simple --template java --name java --project test", + PreRunE: opt.preRunE, + RunE: opt.runE, + } + + flags := cmd.Flags() + flags.StringVarP(&opt.Workspace, "workspace", "", "", + "The workspace name of KubeSphere cluster") + flags.StringVarP(&opt.Workspace, "ws", "", "", + "The workspace name of KubeSphere cluster. This is an alias for --workspace") + flags.StringVarP(&opt.Project, "project", "", "", + "The DevOps project name of KubeSphere cluster") + flags.StringVarP(&opt.Name, "name", "", "", + "The name of the Pipeline") + flags.StringVarP(&opt.Jenkinsfile, "jenkinsfile", "", "", + "The Jenkinsfile of the Pipeline") + flags.StringVarP(&opt.Template, "template", "", "", + "Template of Jenkinsfile include: java, go. This option will override the option --jenkinsfile") + return +} + +func (o *pipelineCreateOption) preRunE(cmd *cobra.Command, args []string) (err error) { + switch o.Template { + case "": + case "java": + o.Jenkinsfile = jenkinsfileTemplateForJava + case "go": + o.Jenkinsfile = jenkinsfileTemplateForGo + default: + err = fmt.Errorf("%s is not support", o.Template) + } + o.Jenkinsfile = strings.TrimSpace(o.Jenkinsfile) + return +} + +func (o *pipelineCreateOption) runE(cmd *cobra.Command, args []string) (err error) { + ctx := context.TODO() + + var ws *unstructured.Unstructured + if ws, err = o.checkWorkspace(); err != nil { + return + } + var project *unstructured.Unstructured + if project, err = o.checkDevOpsProject(ws); err != nil { + return + } + o.Project = project.GetName() // the previous name is the generate name + + var rawPip *unstructured.Unstructured + if rawPip, err = o.createPipelineObj(); err == nil { + if rawPip, err = o.Client.Resource(types.GetPipelineSchema()).Namespace(o.Project).Create(ctx, rawPip, metav1.CreateOptions{}); err != nil { + err = fmt.Errorf("failed to create Pipeline, %v", err) + } + } + return +} + +func (o *pipelineCreateOption) checkWorkspace() (ws *unstructured.Unstructured, err error) { + ctx := context.TODO() + ws, err = o.Client.Resource(types.GetWorkspaceSchema()).Get(ctx, o.Workspace, metav1.GetOptions{}) + // TODO create the workspace if it's not exists + return +} + +func (o *pipelineCreateOption) checkDevOpsProject(ws *unstructured.Unstructured) (project *unstructured.Unstructured, err error) { + ctx := context.TODO() + + selector := labels.Set{"kubesphere.io/workspace": o.Workspace} + var list *unstructured.UnstructuredList + if list, err = o.Client.Resource(types.GetDevOpsProjectSchema()).List(ctx, metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(selector).String(), + }); err != nil { + return + } + + found := false + for i := range list.Items { + if list.Items[i].GetGenerateName() == o.Project { + found = true + project = &list.Items[i] + break + } + } + + if !found { + var tpl *template.Template + o.WorkspaceUID = string(ws.GetUID()) + if tpl, err = template.New("project").Parse(devopsProjectTemplate); err != nil { + return + } + + var buf bytes.Buffer + if err = tpl.Execute(&buf, o); err != nil { + return + } + + var projectObj *unstructured.Unstructured + if projectObj, err = types.GetObjectFromYaml(buf.String()); err != nil { + err = fmt.Errorf("failed to unmarshal yaml to DevOpsProject object, %v", err) + return + } + + project, err = o.Client.Resource(types.GetDevOpsProjectSchema()).Create(ctx, projectObj, metav1.CreateOptions{}) + } + return +} + +func (o *pipelineCreateOption) createPipelineObj() (rawPip *unstructured.Unstructured, err error) { + var tpl *template.Template + funcMap := sprig.FuncMap() + funcMap["raw"] = html.EscapeString + if tpl, err = template.New("pipeline").Funcs(funcMap).Parse(pipelineTemplate); err != nil { + err = fmt.Errorf("failed to parse Pipeline template, %v", err) + return + } + + var buf bytes.Buffer + if err = tpl.Execute(&buf, o); err != nil { + err = fmt.Errorf("failed to render Pipeline template, %v", err) + return + } + + if rawPip, err = types.GetObjectFromYaml(buf.String()); err != nil { + err = fmt.Errorf("failed to unmarshal yaml to Pipeline object, %v", err) + } + return +} + +var devopsProjectTemplate = ` +apiVersion: devops.kubesphere.io/v1alpha3 +kind: DevOpsProject +metadata: + annotations: + kubesphere.io/creator: admin + finalizers: + - devopsproject.finalizers.kubesphere.io + generateName: {{.Project}} + labels: + kubesphere.io/workspace: {{.Workspace}} + ownerReferences: + - apiVersion: tenant.kubesphere.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Workspace + name: {{.Workspace}} + uid: {{.WorkspaceUID}} +` + +var pipelineTemplate = ` +apiVersion: devops.kubesphere.io/v1alpha3 +kind: Pipeline +metadata: + annotations: + kubesphere.io/creator: admin + finalizers: + - pipeline.finalizers.kubesphere.io + name: "{{.Name}}" + namespace: {{.Project}} +spec: + pipeline: + disable_concurrent: true + discarder: + days_to_keep: "7" + num_to_keep: "10" + jenkinsfile: | +{{.Jenkinsfile | indent 6 | raw}} + name: "{{.Name}}" + type: pipeline +status: {} +` + +var jenkinsfileTemplateForJava = ` +pipeline { + agent { + node { + label 'maven' + } + } + stages { + stage('Clone') { + steps { + git(url: 'https://github.com/kubesphere-sigs/demo-java', changelog: true, poll: false) + } + } + stage('Build & Test') { + steps { + container('maven') { + sh 'mvn package test' + } + } + } + stage('Code Scan') { + steps { + withSonarQubeEnv('sonar') { + container('maven') { + sh '''mvn --version +mvn sonar:sonar \\ + -Dsonar.projectKey=test \\ + -Dsonar.host.url=http://139.198.9.130:30687/ \\ + -Dsonar.login=b3e146cdb76ecef5ffb12743779cd78e69a4b5c5''' + } + + } + + waitForQualityGate 'false' + } + } + stage('Build Image') { + steps { + container('maven') { + withCredentials([usernamePassword(credentialsId : 'docker' ,passwordVariable : 'PASS' ,usernameVariable : 'USER' ,)]) { + sh '''docker login -u $USER -p $PASS +cat <Dockerfile +FROM kubesphere/java-8-centos7:v2.1.0 +COPY target/demo-java-1.0.0.jar demo.jar +COPY target/lib demo-lib +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "demo.jar"] +EOM +docker build . -t surenpi/java-demo +docker push surenpi/java-demo''' + } + } + } + } + } +} +` +var jenkinsfileTemplateForGo = ` +pipeline { + agent { + node { + label 'go' + } + } + stages { + stage('Code Clone') { + steps { + git(url: 'https://github.com/kubesphere-sigs/demo-go-http', changelog: true, poll: false) + } + } + stage('Test & Code Scan') { + steps { + container('go') { + sh 'go test ./... -coverprofile=covprofile' + withCredentials([string(credentialsId : 'sonar-token' ,variable : 'TOKEN' ,)]) { + withSonarQubeEnv('sonar') { + sh 'sonar-scanner -Dsonar.login=$TOKEN' + } + } + } + + waitForQualityGate 'false' + } + } + stage('Build Image & Push') { + steps { + container('go') { + sh ''' CGO_ENABLED=0 GOARCH=amd64 go build -o bin/go-server -ldflags "-w" + chmod u+x bin/go-server''' + withCredentials([usernamePassword(credentialsId : 'rick-docker-hub' ,passwordVariable : 'PASS' ,usernameVariable : 'USER' ,)]) { + sh 'echo "$PASS" | docker login -u "$USER" --password-stdin' + sh '''cat <Dockerfile +FROM alpine +COPY bin/go-server go-server +EXPOSE 80 +ENTRYPOINT ["go-server"] +EOM +docker build . -t surenpi/go-demo +docker push surenpi/go-demo''' + } + } + } + } + } +} +` diff --git a/kubectl-plugin/pipeline/del.go b/kubectl-plugin/pipeline/del.go new file mode 100644 index 0000000..f486495 --- /dev/null +++ b/kubectl-plugin/pipeline/del.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "context" + "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +// NewDelPipelineCmd returns a command to delete pipelines +func NewDelPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { + cmd = &cobra.Command{ + Use: "delete", + Aliases: []string{"del", "remove", "rm"}, + Short: "Delete a specific Pipeline of KubeSphere DevOps", + RunE: func(cmd *cobra.Command, args []string) (err error) { + var pips []string + var ns string + if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { + for _, pip := range pips { + fmt.Println(pip) + if err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Delete(context.TODO(), pip, metav1.DeleteOptions{}); err != nil { + break + } + } + } + return + }, + } + return +} diff --git a/kubectl-plugin/pipeline/edit.go b/kubectl-plugin/pipeline/edit.go new file mode 100644 index 0000000..eef920b --- /dev/null +++ b/kubectl-plugin/pipeline/edit.go @@ -0,0 +1,107 @@ +package pipeline + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "os" + "sigs.k8s.io/yaml" +) + +// NewPipelineEditCmd returns a command to edit the pipeline +func NewPipelineEditCmd(client dynamic.Interface) (cmd *cobra.Command) { + ctx := context.TODO() + cmd = &cobra.Command{ + Use: "edit", + Aliases: []string{"e"}, + Short: "Edit the target pipeline", + RunE: func(cmd *cobra.Command, args []string) (err error) { + var pips []string + var ns string + if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { + for _, pip := range pips { + var rawPip *unstructured.Unstructured + var data []byte + buf := bytes.NewBuffer(data) + cmd.Printf("get pipeline %s/%s\n", ns, pip) + if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + if err = enc.Encode(rawPip); err != nil { + return + } + } else { + err = fmt.Errorf("cannot get pipeline, error: %#v", err) + return + } + + var yamlData []byte + if yamlData, err = yaml.JSONToYAML(buf.Bytes()); err != nil { + return + } + + var fileName = "*.yaml" + var content = string(yamlData) + + prompt := &survey.Editor{ + Message: fmt.Sprintf("Edit pipeline %s/%s", ns, pip), + FileName: fileName, + Default: string(yamlData), + HideDefault: true, + AppendDefault: true, + } + + err = survey.AskOne(prompt, &content, survey.WithStdio(os.Stdin, os.Stdout, os.Stderr)) + + if err = yaml.Unmarshal([]byte(content), rawPip); err == nil { + _, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Update(context.TODO(), rawPip, metav1.UpdateOptions{}) + } + } + } + return + }, + } + return +} + +func getPipelinesWithConfirm(client dynamic.Interface, args []string) (ns string, pips []string, err error) { + var allPips []string + if ns, allPips, err = getPipelines(client, args); err != nil { + return + } + + prompt := &survey.MultiSelect{ + Message: "Please select the pipelines that you want to check:", + Options: allPips, + } + err = survey.AskOne(prompt, &pips) + return +} + +func getPipelines(client dynamic.Interface, args []string) (ns string, pips []string, err error) { + if len(args) >= 2 { + ns, pips = args[0], args[1:] + return + } else if len(args) == 1 { + ns = args[0] + } else { + if ns, err = getNamespace(client, args); err != nil { + return + } + } + + var list *unstructured.UnstructuredList + if list, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).List(context.TODO(), metav1.ListOptions{}); err == nil { + for _, item := range list.Items { + pips = append(pips, item.GetName()) + } + } + return +} diff --git a/kubectl-plugin/pipeline/root.go b/kubectl-plugin/pipeline/root.go index 523403b..a71db45 100644 --- a/kubectl-plugin/pipeline/root.go +++ b/kubectl-plugin/pipeline/root.go @@ -1,18 +1,13 @@ package pipeline import ( - "bytes" "context" - "encoding/json" "fmt" "github.com/AlecAivazis/survey/v2" "github.com/linuxsuren/ks/kubectl-plugin/types" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/dynamic" - "os" - "sigs.k8s.io/yaml" ) // NewPipelineCmd returns a command of pipeline @@ -39,160 +34,8 @@ func NewPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { cmd.AddCommand(NewDelPipelineCmd(client), NewPipelineEditCmd(client), - NewPipelineViewCmd(client)) - return -} - -// NewPipelineViewCmd returns a command to view pipeline -func NewPipelineViewCmd(client dynamic.Interface) (cmd *cobra.Command) { - ctx := context.TODO() - cmd = &cobra.Command{ - Use: "view", - Short: "Output the YAML format of a Pipeline", - RunE: func(cmd *cobra.Command, args []string) (err error) { - var pips []string - var ns string - if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { - for _, pip := range pips { - var rawPip *unstructured.Unstructured - var data []byte - buf := bytes.NewBuffer(data) - if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { - enc := json.NewEncoder(buf) - enc.SetIndent("", " ") - if err = enc.Encode(rawPip); err != nil { - return - } - } else { - err = fmt.Errorf("cannot get pipeline, error: %#v", err) - return - } - - var yamlData []byte - if yamlData, err = yaml.JSONToYAML(buf.Bytes()); err != nil { - return - } - - cmd.Println(string(yamlData)) - } - } - return - }, - } - return -} - -// NewDelPipelineCmd returns a command to delete pipelines -func NewDelPipelineCmd(client dynamic.Interface) (cmd *cobra.Command) { - cmd = &cobra.Command{ - Use: "delete", - Aliases: []string{"del", "remove", "rm"}, - Short: "Delete a specific Pipeline of KubeSphere DevOps", - RunE: func(cmd *cobra.Command, args []string) (err error) { - var pips []string - var ns string - if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { - for _, pip := range pips { - fmt.Println(pip) - if err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Delete(context.TODO(), pip, metav1.DeleteOptions{}); err != nil { - break - } - } - } - return - }, - } - return -} - -// NewPipelineEditCmd returns a command to edit the pipeline -func NewPipelineEditCmd(client dynamic.Interface) (cmd *cobra.Command) { - ctx := context.TODO() - cmd = &cobra.Command{ - Use: "edit", - Aliases: []string{"e"}, - Short: "Edit the target pipeline", - RunE: func(cmd *cobra.Command, args []string) (err error) { - var pips []string - var ns string - if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { - for _, pip := range pips { - var rawPip *unstructured.Unstructured - var data []byte - buf := bytes.NewBuffer(data) - cmd.Printf("get pipeline %s/%s\n", ns, pip) - if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { - enc := json.NewEncoder(buf) - enc.SetIndent("", " ") - if err = enc.Encode(rawPip); err != nil { - return - } - } else { - err = fmt.Errorf("cannot get pipeline, error: %#v", err) - return - } - - var yamlData []byte - if yamlData, err = yaml.JSONToYAML(buf.Bytes()); err != nil { - return - } - - var fileName = "*.yaml" - var content = string(yamlData) - - prompt := &survey.Editor{ - Message: fmt.Sprintf("Edit pipeline %s/%s", ns, pip), - FileName: fileName, - Default: string(yamlData), - HideDefault: true, - AppendDefault: true, - } - - err = survey.AskOne(prompt, &content, survey.WithStdio(os.Stdin, os.Stdout, os.Stderr)) - - if err = yaml.Unmarshal([]byte(content), rawPip); err == nil { - _, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Update(context.TODO(), rawPip, metav1.UpdateOptions{}) - } - } - } - return - }, - } - return -} - -func getPipelinesWithConfirm(client dynamic.Interface, args []string) (ns string, pips []string, err error) { - var allPips []string - if ns, allPips, err = getPipelines(client, args); err != nil { - return - } - - prompt := &survey.MultiSelect{ - Message: "Please select the pipelines that you want to check:", - Options: allPips, - } - err = survey.AskOne(prompt, &pips) - return -} - -func getPipelines(client dynamic.Interface, args []string) (ns string, pips []string, err error) { - if len(args) >= 2 { - ns, pips = args[0], args[1:] - return - } else if len(args) == 1 { - ns = args[0] - } else { - if ns, err = getNamespace(client, args); err != nil { - return - } - } - - var list *unstructured.UnstructuredList - if list, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).List(context.TODO(), metav1.ListOptions{}); err == nil { - for _, item := range list.Items { - pips = append(pips, item.GetName()) - } - } + NewPipelineViewCmd(client), + newPipelineCreateCmd(client)) return } diff --git a/kubectl-plugin/pipeline/view.go b/kubectl-plugin/pipeline/view.go new file mode 100644 index 0000000..c03ad13 --- /dev/null +++ b/kubectl-plugin/pipeline/view.go @@ -0,0 +1,53 @@ +package pipeline + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/linuxsuren/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/yaml" +) + +// NewPipelineViewCmd returns a command to view pipeline +func NewPipelineViewCmd(client dynamic.Interface) (cmd *cobra.Command) { + ctx := context.TODO() + cmd = &cobra.Command{ + Use: "view", + Short: "Output the YAML format of a Pipeline", + RunE: func(cmd *cobra.Command, args []string) (err error) { + var pips []string + var ns string + if ns, pips, err = getPipelinesWithConfirm(client, args); err == nil { + for _, pip := range pips { + var rawPip *unstructured.Unstructured + var data []byte + buf := bytes.NewBuffer(data) + if rawPip, err = client.Resource(types.GetPipelineSchema()).Namespace(ns).Get(ctx, pip, metav1.GetOptions{}); err == nil { + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + if err = enc.Encode(rawPip); err != nil { + return + } + } else { + err = fmt.Errorf("cannot get pipeline, error: %#v", err) + return + } + + var yamlData []byte + if yamlData, err = yaml.JSONToYAML(buf.Bytes()); err != nil { + return + } + + cmd.Println(string(yamlData)) + } + } + return + }, + } + return +} diff --git a/kubectl-plugin/types/object.go b/kubectl-plugin/types/object.go new file mode 100644 index 0000000..dfe18a7 --- /dev/null +++ b/kubectl-plugin/types/object.go @@ -0,0 +1,13 @@ +package types + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" +) + +// GetObjectFromYaml returns the Unstructured object from a YAML +func GetObjectFromYaml(yamlText string) (obj *unstructured.Unstructured, err error) { + obj = &unstructured.Unstructured{} + err = yaml.Unmarshal([]byte(yamlText), obj) + return +} diff --git a/kubectl-plugin/types/schema.go b/kubectl-plugin/types/schema.go index 12bad9b..47fa0d7 100644 --- a/kubectl-plugin/types/schema.go +++ b/kubectl-plugin/types/schema.go @@ -20,6 +20,24 @@ func GetPipelineSchema() schema.GroupVersionResource { } } +// GetDevOpsProjectSchema returns the schema of DevOpsProject +func GetDevOpsProjectSchema() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "devops.kubesphere.io", + Version: "v1alpha3", + Resource: "devopsprojects", + } +} + +// GetWorkspaceSchema returns the schema of workspace +func GetWorkspaceSchema() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: "tenant.kubesphere.io", + Version: "v1alpha1", + Resource: "workspaces", + } +} + // GetNamespaceSchema returns the schema of namespaces func GetNamespaceSchema() schema.GroupVersionResource { return schema.GroupVersionResource{