diff --git a/kubectl-plugin/pipeline/create.go b/kubectl-plugin/pipeline/create.go index 782e676..85176a6 100644 --- a/kubectl-plugin/pipeline/create.go +++ b/kubectl-plugin/pipeline/create.go @@ -1,44 +1,23 @@ package pipeline import ( - "bytes" - "context" "fmt" - "github.com/AlecAivazis/survey/v2" - "github.com/Masterminds/sprig" - "github.com/Pallinder/go-randomdata" "github.com/kubesphere-sigs/ks/kubectl-plugin/common" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/option" "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/tpl" - "github.com/kubesphere-sigs/ks/kubectl-plugin/types" "github.com/spf13/cobra" - "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/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" - "strings" ) -type pipelineCreateOption struct { - Workspace string - Project string - Name string - Jenkinsfile string - Template string - Type string - SCMType string - Batch bool - SkipCheck bool - - // Inner fields - Client dynamic.Interface - WorkspaceUID string +type innerPipelineCreateOption struct { + option.PipelineCreateOption } func newPipelineCreateCmd(client dynamic.Interface) (cmd *cobra.Command) { - opt := &pipelineCreateOption{ - Client: client, + opt := &innerPipelineCreateOption{ + PipelineCreateOption: option.PipelineCreateOption{ + Client: client, + }, } cmd = &cobra.Command{ @@ -91,82 +70,16 @@ KubeSphere supports multiple types Pipeline. Currently, this CLI only support th return } -func (o *pipelineCreateOption) wizard(_ *cobra.Command, _ []string) (err error) { - if o.Batch { - // without wizard in batch mode - return - } - - if o.Workspace == "" { - var wsNames []string - if wsNames, err = o.getWorkspaceTemplateNameList(); err == nil { - if o.Workspace, err = chooseObjectFromArray("workspace name", wsNames); err != nil { - return - } - } else { - return - } - } - - if o.Project == "" { - var projectNames []string - if projectNames, err = o.getDevOpsProjectNameList(); err == nil { - if o.Project, err = chooseObjectFromArray("project name", projectNames); err != nil { - return - } - } else { - return - } - } - - if o.Template == "" { - if o.Template, err = chooseOneFromArray(tpl.GetAllTemplates()); err != nil { - return - } - } - - if o.Name == "" { - defaultVal := fmt.Sprintf("%s-%s", o.Template, strings.ToLower(randomdata.SillyName())) - if o.Name, err = getInput("Please input the Pipeline name", defaultVal); err != nil { - return - } - } - return -} - -func chooseObjectFromArray(object string, options []string) (result string, err error) { - prompt := &survey.Select{ - Message: fmt.Sprintf("Please select %s:", object), - Options: options, - } - err = survey.AskOne(prompt, &result) - return -} - -func chooseOneFromArray(options []string) (result string, err error) { - result, err = chooseObjectFromArray("", options) - return -} - -func getInput(title, defaultVal string) (result string, err error) { - prompt := &survey.Input{ - Message: title, - Default: defaultVal, - } - err = survey.AskOne(prompt, &result) - return -} - -func (o *pipelineCreateOption) preRunE(cmd *cobra.Command, args []string) (err error) { +func (o *innerPipelineCreateOption) preRunE(cmd *cobra.Command, args []string) (err error) { if o.Name == "" && len(args) > 0 { o.Name = args[0] } - if err = o.wizard(cmd, args); err != nil { + if err = o.Wizard(cmd, args); err != nil { return } - if err = o.parseTemplate(); err != nil { + if err = o.ParseTemplate(); err != nil { return } @@ -180,339 +93,7 @@ func (o *pipelineCreateOption) preRunE(cmd *cobra.Command, args []string) (err e return } -func (o *pipelineCreateOption) parseTemplate() (err error) { - switch o.Template { - case "": - case "java": - o.Jenkinsfile = tpl.GetBuildJava() - case "go": - o.Jenkinsfile = tpl.GetBuildGo() - case "simple": - o.Jenkinsfile = tpl.GetSimple() - case "parameter": - o.Jenkinsfile = tpl.GetParameter() - case "longRun": - o.Jenkinsfile = tpl.GetLongRunPipeline() - case "parallel": - o.Jenkinsfile = tpl.GetParallel() - case "multi-branch-git": - o.Type = "multi-branch-pipeline" - o.SCMType = "git" - case "multi-branch-gitlab": - o.Type = "multi-branch-pipeline" - o.SCMType = "gitlab" - case "multi-branch-github": - o.Type = "multi-branch-pipeline" - o.SCMType = "github" - default: - err = fmt.Errorf("%s is not support", o.Template) - } - o.Jenkinsfile = strings.TrimSpace(o.Jenkinsfile) +func (o *innerPipelineCreateOption) runE(cmd *cobra.Command, args []string) (err error) { + err = o.CreatePipeline() return } - -func (o *pipelineCreateOption) createPipeline() (err error) { - ctx := context.TODO() - - var wdID string - if !o.SkipCheck { - var ws *unstructured.Unstructured - if ws, err = o.checkWorkspace(); err != nil { - return - } - wdID = string(ws.GetUID()) - } - - var project *unstructured.Unstructured - if project, err = o.checkDevOpsProject(wdID); 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) runE(cmd *cobra.Command, args []string) (err error) { - err = o.createPipeline() - return -} - -func (o *pipelineCreateOption) getDevOpsNamespaceList() (names []string, err error) { - names, err = o.getUnstructuredNameList(true, []string{}, types.GetDevOpsProjectSchema()) - return -} - -func (o *pipelineCreateOption) getDevOpsProjectNameList() (names []string, err error) { - names, err = o.getUnstructuredNameList(false, []string{}, types.GetDevOpsProjectSchema()) - return -} - -func (o *pipelineCreateOption) getWorkspaceNameList() (names []string, err error) { - names, err = o.getUnstructuredNameList(true, []string{"system-workspace"}, types.GetWorkspaceSchema()) - return -} - -func (o *pipelineCreateOption) getWorkspaceTemplateNameList() (names []string, err error) { - names, err = o.getUnstructuredNameList(true, []string{"system-workspace"}, types.GetWorkspaceTemplate()) - return -} - -func (o *pipelineCreateOption) getUnstructuredNameListInNamespace(namespace string, originalName bool, excludes []string, schemaType schema.GroupVersionResource) (names []string, err error) { - var wsList *unstructured.UnstructuredList - if namespace != "" { - wsList, err = o.getUnstructuredListInNamespace(namespace, schemaType) - } else { - wsList, err = o.getUnstructuredList(schemaType) - } - - if err == nil { - names = make([]string, 0) - for i := range wsList.Items { - var name string - if originalName { - name = wsList.Items[i].GetName() - } else { - name = wsList.Items[i].GetGenerateName() - } - - exclude := false - for j := range excludes { - if name == excludes[j] { - exclude = true - break - } - } - - if !exclude { - names = append(names, name) - } - } - } - return -} - -func (o *pipelineCreateOption) getUnstructuredNameList(originalName bool, excludes []string, schemaType schema.GroupVersionResource) (names []string, err error) { - return o.getUnstructuredNameListInNamespace("", originalName, excludes, schemaType) -} - -func (o *pipelineCreateOption) getUnstructuredListInNamespace(namespace string, schemaType schema.GroupVersionResource) ( - wsList *unstructured.UnstructuredList, err error) { - ctx := context.TODO() - wsList, err = o.Client.Resource(schemaType).Namespace(namespace).List(ctx, metav1.ListOptions{}) - return -} - -func (o *pipelineCreateOption) getUnstructuredList(schemaType schema.GroupVersionResource) (wsList *unstructured.UnstructuredList, err error) { - ctx := context.TODO() - wsList, err = o.Client.Resource(schemaType).List(ctx, metav1.ListOptions{}) - return -} - -func (o *pipelineCreateOption) getWorkspaceList() (wsList *unstructured.UnstructuredList, err error) { - wsList, err = o.getUnstructuredList(types.GetWorkspaceSchema()) - return -} - -func (o *pipelineCreateOption) getWorkspaceTemplateList() (wsList *unstructured.UnstructuredList, err error) { - wsList, err = o.getUnstructuredList(types.GetWorkspaceTemplate()) - return -} - -func (o *pipelineCreateOption) checkWorkspace() (ws *unstructured.Unstructured, err error) { - ctx := context.TODO() - if ws, err = o.Client.Resource(types.GetWorkspaceSchema()).Get(ctx, o.Workspace, metav1.GetOptions{}); err == nil { - return - } - - // TODO check workspaceTemplate when ks in a multi-cluster environment - if ws, err = o.Client.Resource(types.GetWorkspaceTemplate()).Get(ctx, o.Workspace, metav1.GetOptions{}); err != nil { - // create workspacetemplate - var wsTemplate *unstructured.Unstructured - if wsTemplate, err = types.GetObjectFromYaml(fmt.Sprintf(`apiVersion: tenant.kubesphere.io/v1alpha2 -kind: WorkspaceTemplate -metadata: - name: %s`, o.Workspace)); err != nil { - err = fmt.Errorf("failed to unmarshal yaml to DevOpsProject object, %v", err) - return - } - - ws, err = o.Client.Resource(types.GetWorkspaceTemplate()).Create(ctx, wsTemplate, metav1.CreateOptions{}) - } - return -} - -func (o *pipelineCreateOption) getDevOpsProjectGenerateNameList() (names []string, err error) { - var list *unstructured.UnstructuredList - if list, err = o.getDevOpsProjectList(); err != nil { - return - } - - names = make([]string, len(list.Items)) - for i := range list.Items { - names[i] = list.Items[i].GetGenerateName() - } - return -} - -func (o *pipelineCreateOption) getDevOpsProjectList() (wsList *unstructured.UnstructuredList, err error) { - ctx := context.TODO() - selector := labels.Set{"kubesphere.io/workspace": o.Workspace} - wsList, err = o.Client.Resource(types.GetDevOpsProjectSchema()).List(ctx, metav1.ListOptions{ - LabelSelector: labels.SelectorFromSet(selector).String(), - }) - return -} - -func (o *pipelineCreateOption) checkDevOpsProject(wsID string) (project *unstructured.Unstructured, err error) { - ctx := context.TODO() - var list *unstructured.UnstructuredList - if list, err = o.getDevOpsProjectList(); 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 = wsID - if tpl, err = template.New("project").Parse(devopsProjectTemplate); err != nil { - err = fmt.Errorf("failed to parse devops project template, error is: %v", err) - 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 - } - - if project, err = o.Client.Resource(types.GetDevOpsProjectSchema()).Create(ctx, projectObj, metav1.CreateOptions{}); err != nil { - err = fmt.Errorf("failed to create devops project with YAML: '%s'. Error is: %v", buf.String(), err) - } - } - return -} - -func (o *pipelineCreateOption) createPipelineObj() (rawPip *unstructured.Unstructured, err error) { - var tpl *template.Template - funcMap := sprig.FuncMap() - //funcMap["raw"] = html.UnescapeString - funcMap["raw"] = func(text string) template.HTML { - /* #nosec */ - return template.HTML(text) - } - 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}} - {{if ne .WorkspaceUID ""}} - ownerReferences: - - apiVersion: tenant.kubesphere.io/v1alpha1 - blockOwnerDeletion: true - controller: true - kind: Workspace - name: {{.Workspace}} - uid: {{.WorkspaceUID}} - {{end}} -` - -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: - {{if eq .Type "pipeline"}} - pipeline: - disable_concurrent: true - discarder: - days_to_keep: "7" - num_to_keep: "10" - jenkinsfile: | -{{.Jenkinsfile | indent 6 | raw}} - name: "{{.Name}}" - {{else if eq .Type "multi-branch-pipeline" -}} - multi_branch_pipeline: - discarder: - days_to_keep: "-1" - num_to_keep: "-1" - {{if eq .SCMType "gitlab"}} - gitlab_source: - discover_branches: 1 - discover_pr_from_forks: - strategy: 2 - trust: 2 - discover_pr_from_origin: 2 - discover_tags: true - owner: devops-ws - repo: devops-ws/learn-pipeline-java - server_name: https://gitlab.com - {{else if eq .SCMType "github" -}} - github_source: - discover_branches: 1 - discover_pr_from_forks: - strategy: 2 - trust: 2 - discover_pr_from_origin: 2 - discover_tags: true - owner: devops-ws - repo: learn-pipeline-java - {{else if eq .SCMType "git" -}} - git_source: - discover_branches: true - url: https://gitee.com/devops-ws/learn-pipeline-java - {{end -}} - name: "{{.Name}}" - script_path: Jenkinsfile - source_type: {{.SCMType}} - {{end -}} - type: {{.Type}} -status: {} -` diff --git a/kubectl-plugin/pipeline/dashboard.go b/kubectl-plugin/pipeline/dashboard.go index 0ddeaa6..8c8aab9 100644 --- a/kubectl-plugin/pipeline/dashboard.go +++ b/kubectl-plugin/pipeline/dashboard.go @@ -7,18 +7,17 @@ import ( "github.com/Pallinder/go-randomdata" "github.com/gdamore/tcell/v2" "github.com/kubesphere-sigs/ks/kubectl-plugin/common" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/option" "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/tpl" "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/ui" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/ui/project" "github.com/kubesphere-sigs/ks/kubectl-plugin/types" "github.com/rivo/tview" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -216,7 +215,7 @@ func (o *dashboardOption) pipelineCreationForm() { templateField := templateItem.(*tview.DropDown) _, templateName := templateField.GetCurrentOption() - opt := &pipelineCreateOption{ + opt := &option.PipelineCreateOption{ Name: nameField.GetText(), Project: o.namespaceProjectMap[o.namespace], Template: templateName, @@ -225,8 +224,8 @@ func (o *dashboardOption) pipelineCreationForm() { Type: "pipeline", Client: o.client, } - _ = opt.parseTemplate() - _ = opt.createPipeline() // need to find a way to show the errors + _ = opt.ParseTemplate() + _ = opt.CreatePipeline() // need to find a way to show the errors } o.stack.Pop() @@ -260,43 +259,15 @@ func (o *dashboardOption) listPipelines(index int, mainText string, secondaryTex } func (o *dashboardOption) createNamespaceList() (listView tview.Primitive) { - list := tview.NewList() - list.SetBorder(true).SetTitle("namespaces") - go func() { - if watchEvent, err := o.client.Resource(types.GetNamespaceSchema()).Watch(context.TODO(), metav1.ListOptions{ - LabelSelector: "kubesphere.io/devopsproject", - }); err == nil { - for event := range watchEvent.ResultChan() { - switch event.Type { - case watch.Added: - unss := event.Object.(*unstructured.Unstructured) - ss := &corev1.Namespace{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unss.Object, ss); err == nil { - list.AddItem(ss.Name, "", 0, nil) - } - - if devopsProject, err := o.client.Resource(types.GetDevOpsProjectSchema()). - Get(context.TODO(), ss.Name, metav1.GetOptions{}); err == nil { - o.namespaceWorkspaceMap[ss.Name] = devopsProject.GetLabels()["kubesphere.io/workspace"] - o.namespaceProjectMap[ss.Name] = devopsProject.GetGenerateName() - } - case watch.Deleted: - for i := 0; i < list.GetItemCount(); i++ { - name, _ := list.GetItemText(i) - unss := event.Object.(*unstructured.Unstructured) - ss := &corev1.Namespace{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unss.Object, ss); err == nil { - if name == ss.Name { - list.RemoveItem(i) - break - } - } - } - } - o.app.Draw() - } + list := ui.NewResourceList(o.client, o.app, o.stack) + list.PutItemAddingListener(func(name string) { + if devopsProject, err := o.client.Resource(types.GetDevOpsProjectSchema()). + Get(context.TODO(), name, metav1.GetOptions{}); err == nil { + o.namespaceWorkspaceMap[name] = devopsProject.GetLabels()["kubesphere.io/workspace"] + o.namespaceProjectMap[name] = devopsProject.GetGenerateName() } - }() + }) + list.Load("", types.GetNamespaceSchema(), "kubesphere.io/devopsproject") list.SetChangedFunc(o.listPipelines) o.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch key := event.Rune(); key { @@ -311,7 +282,25 @@ func (o *dashboardOption) createNamespaceList() (listView tview.Primitive) { } return event }) + inputCapture := list.GetInputCapture() + list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch key := event.Rune(); key { + case 'p': + o.createProject() + } + return inputCapture(event) + }) o.app.SetFocus(list) listView = list return } + +func (o *dashboardOption) createProject() { + form := project.NewProjectForm(o.client) + form.SetConfirmEvent(func() { + o.stack.Pop() + }).SetCancelEvent(func() { + o.stack.Pop() + }) + o.stack.Push(form) +} diff --git a/kubectl-plugin/pipeline/gc.go b/kubectl-plugin/pipeline/gc.go index 24cabf8..43d9075 100644 --- a/kubectl-plugin/pipeline/gc.go +++ b/kubectl-plugin/pipeline/gc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/kubesphere-sigs/ks/kubectl-plugin/common" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/option" "github.com/kubesphere-sigs/ks/kubectl-plugin/types" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,7 +17,7 @@ import ( func newGCCmd(client dynamic.Interface) (cmd *cobra.Command) { opt := &gcOption{ client: client, - pipelineCreateOption: pipelineCreateOption{ + PipelineCreateOption: option.PipelineCreateOption{ Client: client, }, } @@ -55,7 +56,7 @@ type gcOption struct { // inner fields client dynamic.Interface - pipelineCreateOption + option.PipelineCreateOption } func (o *gcOption) preRunE(cmd *cobra.Command, args []string) (err error) { @@ -67,7 +68,7 @@ func (o *gcOption) preRunE(cmd *cobra.Command, args []string) (err error) { func (o *gcOption) cleanPipelineRunInNamespace(namespace string) (err error) { var pipelineList *unstructured.UnstructuredList - if pipelineList, err = o.getUnstructuredListInNamespace(namespace, types.GetPipelineRunSchema()); err != nil { + if pipelineList, err = o.GetUnstructuredListInNamespace(namespace, types.GetPipelineRunSchema()); err != nil { err = fmt.Errorf("failed to get PipelineRun list, error: %v", err) return } diff --git a/kubectl-plugin/pipeline/option/create.go b/kubectl-plugin/pipeline/option/create.go new file mode 100644 index 0000000..f89026e --- /dev/null +++ b/kubectl-plugin/pipeline/option/create.go @@ -0,0 +1,446 @@ +package option + +import ( + "bytes" + "context" + "fmt" + "github.com/AlecAivazis/survey/v2" + "github.com/Masterminds/sprig" + "github.com/Pallinder/go-randomdata" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/tpl" + "github.com/kubesphere-sigs/ks/kubectl-plugin/types" + "github.com/spf13/cobra" + "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/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "strings" +) + +// PipelineCreateOption is the option for creating a pipeline +type PipelineCreateOption struct { + Workspace string + Project string + Name string + Jenkinsfile string + Template string + Type string + SCMType string + Batch bool + SkipCheck bool + + // Inner fields + Client dynamic.Interface + WorkspaceUID string +} + +// Wizard is the wizard for creating a pipeline +func (o *PipelineCreateOption) Wizard(_ *cobra.Command, _ []string) (err error) { + if o.Batch { + // without Wizard in batch mode + return + } + + if o.Workspace == "" { + var wsNames []string + if wsNames, err = o.GetWorkspaceTemplateNameList(); err == nil { + if o.Workspace, err = ChooseObjectFromArray("workspace name", wsNames); err != nil { + return + } + } else { + return + } + } + + if o.Project == "" { + var projectNames []string + if projectNames, err = o.getDevOpsProjectNameList(); err == nil { + if o.Project, err = ChooseObjectFromArray("project name", projectNames); err != nil { + return + } + } else { + return + } + } + + if o.Template == "" { + if o.Template, err = ChooseOneFromArray(tpl.GetAllTemplates()); err != nil { + return + } + } + + if o.Name == "" { + defaultVal := fmt.Sprintf("%s-%s", o.Template, strings.ToLower(randomdata.SillyName())) + if o.Name, err = getInput("Please input the Pipeline name", defaultVal); err != nil { + return + } + } + return +} + +// ChooseObjectFromArray chooses a object from array +func ChooseObjectFromArray(object string, options []string) (result string, err error) { + prompt := &survey.Select{ + Message: fmt.Sprintf("Please select %s:", object), + Options: options, + } + err = survey.AskOne(prompt, &result) + return +} + +// ChooseOneFromArray choose an item from array +func ChooseOneFromArray(options []string) (result string, err error) { + result, err = ChooseObjectFromArray("", options) + return +} + +func getInput(title, defaultVal string) (result string, err error) { + prompt := &survey.Input{ + Message: title, + Default: defaultVal, + } + err = survey.AskOne(prompt, &result) + return +} + +// ParseTemplate parses a template +func (o *PipelineCreateOption) ParseTemplate() (err error) { + switch o.Template { + case "": + case "java": + o.Jenkinsfile = tpl.GetBuildJava() + case "go": + o.Jenkinsfile = tpl.GetBuildGo() + case "simple": + o.Jenkinsfile = tpl.GetSimple() + case "parameter": + o.Jenkinsfile = tpl.GetParameter() + case "longRun": + o.Jenkinsfile = tpl.GetLongRunPipeline() + case "parallel": + o.Jenkinsfile = tpl.GetParallel() + case "multi-branch-git": + o.Type = "multi-branch-pipeline" + o.SCMType = "git" + case "multi-branch-gitlab": + o.Type = "multi-branch-pipeline" + o.SCMType = "gitlab" + case "multi-branch-github": + o.Type = "multi-branch-pipeline" + o.SCMType = "github" + default: + err = fmt.Errorf("%s is not support", o.Template) + } + o.Jenkinsfile = strings.TrimSpace(o.Jenkinsfile) + return +} + +// CreatePipeline creates pipeline +func (o *PipelineCreateOption) CreatePipeline() (err error) { + ctx := context.TODO() + + var wdID string + if !o.SkipCheck { + var ws *unstructured.Unstructured + if ws, err = o.CheckWorkspace(); err != nil { + return + } + wdID = string(ws.GetUID()) + } + + var project *unstructured.Unstructured + if project, err = o.CheckDevOpsProject(wdID); 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 +} + +// GetDevOpsNamespaceList returns a DevOps namespace list +func (o *PipelineCreateOption) GetDevOpsNamespaceList() (names []string, err error) { + names, err = o.getUnstructuredNameList(true, []string{}, types.GetDevOpsProjectSchema()) + return +} + +func (o *PipelineCreateOption) getDevOpsProjectNameList() (names []string, err error) { + names, err = o.getUnstructuredNameList(false, []string{}, types.GetDevOpsProjectSchema()) + return +} + +func (o *PipelineCreateOption) getWorkspaceNameList() (names []string, err error) { + names, err = o.getUnstructuredNameList(true, []string{"system-workspace"}, types.GetWorkspaceSchema()) + return +} + +// GetWorkspaceTemplateNameList returns a template name list +func (o *PipelineCreateOption) GetWorkspaceTemplateNameList() (names []string, err error) { + names, err = o.getUnstructuredNameList(true, []string{"system-workspace"}, types.GetWorkspaceTemplate()) + return +} + +// GetUnstructuredNameListInNamespace returns a list +func (o *PipelineCreateOption) GetUnstructuredNameListInNamespace(namespace string, originalName bool, excludes []string, schemaType schema.GroupVersionResource) (names []string, err error) { + var wsList *unstructured.UnstructuredList + if namespace != "" { + wsList, err = o.GetUnstructuredListInNamespace(namespace, schemaType) + } else { + wsList, err = o.getUnstructuredList(schemaType) + } + + if err == nil { + names = make([]string, 0) + for i := range wsList.Items { + var name string + if originalName { + name = wsList.Items[i].GetName() + } else { + name = wsList.Items[i].GetGenerateName() + } + + exclude := false + for j := range excludes { + if name == excludes[j] { + exclude = true + break + } + } + + if !exclude { + names = append(names, name) + } + } + } + return +} + +func (o *PipelineCreateOption) getUnstructuredNameList(originalName bool, excludes []string, schemaType schema.GroupVersionResource) (names []string, err error) { + return o.GetUnstructuredNameListInNamespace("", originalName, excludes, schemaType) +} + +// GetUnstructuredListInNamespace returns the list +func (o *PipelineCreateOption) GetUnstructuredListInNamespace(namespace string, schemaType schema.GroupVersionResource) ( + wsList *unstructured.UnstructuredList, err error) { + ctx := context.TODO() + wsList, err = o.Client.Resource(schemaType).Namespace(namespace).List(ctx, metav1.ListOptions{}) + return +} + +func (o *PipelineCreateOption) getUnstructuredList(schemaType schema.GroupVersionResource) (wsList *unstructured.UnstructuredList, err error) { + ctx := context.TODO() + wsList, err = o.Client.Resource(schemaType).List(ctx, metav1.ListOptions{}) + return +} + +func (o *PipelineCreateOption) getWorkspaceList() (wsList *unstructured.UnstructuredList, err error) { + wsList, err = o.getUnstructuredList(types.GetWorkspaceSchema()) + return +} + +func (o *PipelineCreateOption) getWorkspaceTemplateList() (wsList *unstructured.UnstructuredList, err error) { + wsList, err = o.getUnstructuredList(types.GetWorkspaceTemplate()) + return +} + +// CheckWorkspace makes sure the target workspace exist +func (o *PipelineCreateOption) CheckWorkspace() (ws *unstructured.Unstructured, err error) { + ctx := context.TODO() + if ws, err = o.Client.Resource(types.GetWorkspaceSchema()).Get(ctx, o.Workspace, metav1.GetOptions{}); err == nil { + return + } + + // TODO check workspaceTemplate when ks in a multi-cluster environment + if ws, err = o.Client.Resource(types.GetWorkspaceTemplate()).Get(ctx, o.Workspace, metav1.GetOptions{}); err != nil { + // create workspacetemplate + var wsTemplate *unstructured.Unstructured + if wsTemplate, err = types.GetObjectFromYaml(fmt.Sprintf(`apiVersion: tenant.kubesphere.io/v1alpha2 +kind: WorkspaceTemplate +metadata: + name: %s`, o.Workspace)); err != nil { + err = fmt.Errorf("failed to unmarshal yaml to DevOpsProject object, %v", err) + return + } + + ws, err = o.Client.Resource(types.GetWorkspaceTemplate()).Create(ctx, wsTemplate, metav1.CreateOptions{}) + } + return +} + +func (o *PipelineCreateOption) getDevOpsProjectGenerateNameList() (names []string, err error) { + var list *unstructured.UnstructuredList + if list, err = o.getDevOpsProjectList(); err != nil { + return + } + + names = make([]string, len(list.Items)) + for i := range list.Items { + names[i] = list.Items[i].GetGenerateName() + } + return +} + +func (o *PipelineCreateOption) getDevOpsProjectList() (wsList *unstructured.UnstructuredList, err error) { + ctx := context.TODO() + selector := labels.Set{"kubesphere.io/workspace": o.Workspace} + wsList, err = o.Client.Resource(types.GetDevOpsProjectSchema()).List(ctx, metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(selector).String(), + }) + return +} + +// CheckDevOpsProject makes sure the project exist +func (o *PipelineCreateOption) CheckDevOpsProject(wsID string) (project *unstructured.Unstructured, err error) { + ctx := context.TODO() + var list *unstructured.UnstructuredList + if list, err = o.getDevOpsProjectList(); 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 = wsID + if tpl, err = template.New("project").Parse(devopsProjectTemplate); err != nil { + err = fmt.Errorf("failed to parse devops project template, error is: %v", err) + 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 + } + + if project, err = o.Client.Resource(types.GetDevOpsProjectSchema()).Create(ctx, projectObj, metav1.CreateOptions{}); err != nil { + err = fmt.Errorf("failed to create devops project with YAML: '%s'. Error is: %v", buf.String(), err) + } + } + return +} + +func (o *PipelineCreateOption) createPipelineObj() (rawPip *unstructured.Unstructured, err error) { + var tpl *template.Template + funcMap := sprig.FuncMap() + //funcMap["raw"] = html.UnescapeString + funcMap["raw"] = func(text string) template.HTML { + /* #nosec */ + return template.HTML(text) + } + 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}} + {{if ne .WorkspaceUID ""}} + ownerReferences: + - apiVersion: tenant.kubesphere.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: Workspace + name: {{.Workspace}} + uid: {{.WorkspaceUID}} + {{end}} +` + +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: + {{if eq .Type "pipeline"}} + pipeline: + disable_concurrent: true + discarder: + days_to_keep: "7" + num_to_keep: "10" + jenkinsfile: | +{{.Jenkinsfile | indent 6 | raw}} + name: "{{.Name}}" + {{else if eq .Type "multi-branch-pipeline" -}} + multi_branch_pipeline: + discarder: + days_to_keep: "-1" + num_to_keep: "-1" + {{if eq .SCMType "gitlab"}} + gitlab_source: + discover_branches: 1 + discover_pr_from_forks: + strategy: 2 + trust: 2 + discover_pr_from_origin: 2 + discover_tags: true + owner: devops-ws + repo: devops-ws/learn-pipeline-java + server_name: https://gitlab.com + {{else if eq .SCMType "github" -}} + github_source: + discover_branches: 1 + discover_pr_from_forks: + strategy: 2 + trust: 2 + discover_pr_from_origin: 2 + discover_tags: true + owner: devops-ws + repo: learn-pipeline-java + {{else if eq .SCMType "git" -}} + git_source: + discover_branches: true + url: https://gitee.com/devops-ws/learn-pipeline-java + {{end -}} + name: "{{.Name}}" + script_path: Jenkinsfile + source_type: {{.SCMType}} + {{end -}} + type: {{.Type}} +status: {} +` diff --git a/kubectl-plugin/pipeline/run.go b/kubectl-plugin/pipeline/run.go index aa4dce7..eacb047 100644 --- a/kubectl-plugin/pipeline/run.go +++ b/kubectl-plugin/pipeline/run.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "github.com/kubesphere-sigs/ks/kubectl-plugin/common" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/option" "github.com/kubesphere-sigs/ks/kubectl-plugin/types" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,7 +44,7 @@ type pipelineRunOpt struct { // inner fields client dynamic.Interface - pipelineCreateOption + option.PipelineCreateOption } func (o *pipelineRunOpt) triggerPipeline(ns, pipeline string, parameters map[string]string) (err error) { @@ -71,7 +72,7 @@ func (o *pipelineRunOpt) triggerPipeline(ns, pipeline string, parameters map[str func (o *pipelineRunOpt) preRunE(cmd *cobra.Command, args []string) (err error) { o.client = common.GetDynamicClient(cmd.Root().Context()) - o.pipelineCreateOption.Client = o.client + o.PipelineCreateOption.Client = o.client if o.pipeline == "" && len(args) > 0 { o.pipeline = args[0] @@ -106,8 +107,8 @@ func (o *pipelineRunOpt) wizard(_ *cobra.Command, _ []string) (err error) { if o.Workspace == "" { var wsNames []string - if wsNames, err = o.getWorkspaceTemplateNameList(); err == nil { - if o.Workspace, err = chooseObjectFromArray("workspace name", wsNames); err != nil { + if wsNames, err = o.GetWorkspaceTemplateNameList(); err == nil { + if o.Workspace, err = option.ChooseObjectFromArray("workspace name", wsNames); err != nil { return } } else { @@ -117,8 +118,8 @@ func (o *pipelineRunOpt) wizard(_ *cobra.Command, _ []string) (err error) { if o.namespace == "" { var projectNames []string - if projectNames, err = o.getDevOpsNamespaceList(); err == nil { - if o.namespace, err = chooseObjectFromArray("project name", projectNames); err != nil { + if projectNames, err = o.GetDevOpsNamespaceList(); err == nil { + if o.namespace, err = option.ChooseObjectFromArray("project name", projectNames); err != nil { return } } else { @@ -129,7 +130,7 @@ func (o *pipelineRunOpt) wizard(_ *cobra.Command, _ []string) (err error) { if o.pipeline == "" { var pipelineNames []string if pipelineNames, err = o.getPipelineNameList(); err == nil && len(pipelineNames) > 0 { - if o.pipeline, err = chooseObjectFromArray("pipeline name", pipelineNames); err != nil { + if o.pipeline, err = option.ChooseObjectFromArray("pipeline name", pipelineNames); err != nil { return } } else if len(pipelineNames) == 0 { @@ -157,7 +158,7 @@ func (o *pipelineRunOpt) getDevOpsProjectByGenerateName(name string) (result *un } func (o *pipelineRunOpt) getPipelineNameList() (names []string, err error) { - names, err = o.getUnstructuredNameListInNamespace(o.namespace, true, []string{}, types.GetPipelineSchema()) + names, err = o.GetUnstructuredNameListInNamespace(o.namespace, true, []string{}, types.GetPipelineSchema()) return } diff --git a/kubectl-plugin/pipeline/ui/dialog/delete.go b/kubectl-plugin/pipeline/ui/dialog/delete.go new file mode 100644 index 0000000..9ae451c --- /dev/null +++ b/kubectl-plugin/pipeline/ui/dialog/delete.go @@ -0,0 +1,23 @@ +package dialog + +import "github.com/rivo/tview" + +type ( + okFunc func() + cancelFunc func() +) + +// ShowDelete pops a resource deletion dialog. +func ShowDelete(msg string, ok okFunc, cancel cancelFunc) *tview.Modal { + confirm := tview.NewModal() + confirm.SetText(msg) + confirm.AddButtons([]string{"OK", "Cancel"}) + confirm.SetDoneFunc(func(_ int, label string) { + switch label { + case "OK": + ok() + } + cancel() + }) + return confirm +} diff --git a/kubectl-plugin/pipeline/ui/project/form.go b/kubectl-plugin/pipeline/ui/project/form.go new file mode 100644 index 0000000..0dbeb89 --- /dev/null +++ b/kubectl-plugin/pipeline/ui/project/form.go @@ -0,0 +1,83 @@ +package project + +import ( + "github.com/gdamore/tcell/v2" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/option" + "github.com/rivo/tview" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +// DevOpsProjectForm represents a form to create DevOps project +type DevOpsProjectForm struct { + *tview.Form + + eventConfirmCallback EventCallback + eventCancelCallback EventCallback +} + +// NewProjectForm creates the form +func NewProjectForm(client dynamic.Interface) *DevOpsProjectForm { + form := tview.NewForm() + form.AddInputField("Workspace Name", "", 20, nil, nil) + form.AddInputField("Project Name", "", 20, nil, nil) + form.SetTitle("Create a new Project").SetBorder(true) + + projectForm := &DevOpsProjectForm{ + Form: form, + eventConfirmCallback: doNothing, + eventCancelCallback: doNothing, + } + + form.AddButton("OK", func() { + wsItem := form.GetFormItemByLabel("Workspace Name") + projectItem := form.GetFormItemByLabel("Project Name") + if wsItem != nil && projectItem != nil { + wsField := wsItem.(*tview.InputField) + projectField := projectItem.(*tview.InputField) + + opt := &option.PipelineCreateOption{ + Project: projectField.GetText(), + Workspace: wsField.GetText(), + Batch: true, + Type: "pipeline", + Client: client, + } + + var ws *unstructured.Unstructured + var err error + if ws, err = opt.CheckWorkspace(); err != nil { + return + } + _, _ = opt.CheckDevOpsProject(string(ws.GetUID())) + } + + projectForm.eventConfirmCallback() + }) + form.AddButton("Cancel", projectForm.eventCancelCallback) + form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyEsc: + projectForm.eventCancelCallback() + } + return event + }) + return projectForm +} + +// SetConfirmEvent set the callback function for confirm event +func (p *DevOpsProjectForm) SetConfirmEvent(callback EventCallback) *DevOpsProjectForm { + p.eventConfirmCallback = callback + return p +} + +// SetCancelEvent set the callback function for cancel event +func (p *DevOpsProjectForm) SetCancelEvent(callback EventCallback) *DevOpsProjectForm { + p.eventCancelCallback = callback + return p +} + +// EventCallback is the function for event callback +type EventCallback func() + +var doNothing = func() {} diff --git a/kubectl-plugin/pipeline/ui/resource_list.go b/kubectl-plugin/pipeline/ui/resource_list.go new file mode 100644 index 0000000..0ad19a3 --- /dev/null +++ b/kubectl-plugin/pipeline/ui/resource_list.go @@ -0,0 +1,124 @@ +package ui + +import ( + "context" + "fmt" + "github.com/gdamore/tcell/v2" + "github.com/kubesphere-sigs/ks/kubectl-plugin/pipeline/ui/dialog" + "github.com/rivo/tview" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" +) + +// ResourceList represents a list of a Kubernetes resource +type ResourceList struct { + *tview.List + + //client *rest.RESTClient + client dynamic.Interface + app *tview.Application + stack *Stack + + // inner fields + itemAddingListeners []ItemAddingListener + watch watch.Interface + resource schema.GroupVersionResource +} + +// NewResourceList creates a list for Kubernetes resource +func NewResourceList(client dynamic.Interface, app *tview.Application, stack *Stack) *ResourceList { + list := tview.NewList() + list.SetBorder(true) + + resourceList := &ResourceList{ + List: list, + client: client, + app: app, + stack: stack, + } + list.SetInputCapture(resourceList.eventHandler) + return resourceList +} + +// Load loads the data +func (r *ResourceList) Load(ns string, resource schema.GroupVersionResource, labelSelector string) { + r.Stop().Clear() + r.SetTitle("loading") + r.resource = resource + + go func() { + var err error + if r.watch, err = r.client.Resource(r.resource).Namespace(ns).Watch(context.TODO(), metav1.ListOptions{ + LabelSelector: labelSelector, + }); err == nil { + for event := range r.watch.ResultChan() { + switch event.Type { + case watch.Added: + unss := event.Object.(*unstructured.Unstructured) + r.AddItem(unss.GetName(), "", 0, nil) + r.callItemAddingListener(unss.GetName()) + case watch.Deleted: + for i := 0; i < r.GetItemCount(); i++ { + name, _ := r.GetItemText(i) + unss := event.Object.(*unstructured.Unstructured) + ss := &corev1.Namespace{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unss.Object, ss); err == nil { + if name == ss.Name { + r.RemoveItem(i) + break + } + } + } + } + r.SetTitle(fmt.Sprintf("%s[%d]", resource.Resource, r.GetItemCount())) + r.app.Draw() + } + } + }() +} + +func (r *ResourceList) eventHandler(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyCtrlD: + name, _ := r.GetItemText(r.GetCurrentItem()) + r.stack.Push(dialog.ShowDelete(fmt.Sprintf("Delete %s [%s]?", r.resource.Resource, name), func() { + r.stack.Pop() + r.deleteItemAndResource(name) + }, func() { + r.stack.Pop() + })) + } + return event +} + +func (r *ResourceList) deleteItemAndResource(name string) { + _ = r.client.Resource(r.resource).Delete(context.TODO(), name, metav1.DeleteOptions{}) +} + +func (r *ResourceList) callItemAddingListener(name string) { + for _, listener := range r.itemAddingListeners { + listener(name) + } +} + +// PutItemAddingListener puts an ItemAddingListener +func (r *ResourceList) PutItemAddingListener(listener ItemAddingListener) { + r.itemAddingListeners = append(r.itemAddingListeners, listener) +} + +// Stop stops reload the data +func (r *ResourceList) Stop() *ResourceList { + if r.watch != nil { + r.watch.Stop() + r.watch = nil + } + return r +} + +// ItemAddingListener callback when adding an item +type ItemAddingListener func(string) diff --git a/kubectl-plugin/pipeline/ui/resource_table.go b/kubectl-plugin/pipeline/ui/resource_table.go index 6294656..1631b76 100644 --- a/kubectl-plugin/pipeline/ui/resource_table.go +++ b/kubectl-plugin/pipeline/ui/resource_table.go @@ -46,7 +46,7 @@ func (t *ResourceTable) Load(ns, kind, labelSelector string) { t.ticker = time.NewTicker(time.Second * 2) go func() { - ctx := context.TODO() + ctx := context.TODO() // give it an initial data setting t.reload(ctx, ns, kind, labelSelector)