diff --git a/cmd/plugin/jenkins-pipeline-kubernetes/main.go b/cmd/plugin/jenkins-pipeline-kubernetes/main.go new file mode 100644 index 000000000..b33e0b50a --- /dev/null +++ b/cmd/plugin/jenkins-pipeline-kubernetes/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "github.com/devstream-io/devstream/internal/pkg/plugin/jenkinspipelinekubernetes" + "github.com/devstream-io/devstream/pkg/util/log" +) + +// NAME is the name of this DevStream plugin. +const NAME = "jenkins-pipeline-kubernetes" + +// Plugin is the type used by DevStream core. It's a string. +type Plugin string + +// Create implements the create of jenkins-pipeline-kubernetes. +func (p Plugin) Create(options map[string]interface{}) (map[string]interface{}, error) { + return jenkinspipelinekubernetes.Create(options) +} + +// Update implements the update of jenkins-pipeline-kubernetes. +func (p Plugin) Update(options map[string]interface{}) (map[string]interface{}, error) { + return jenkinspipelinekubernetes.Update(options) +} + +// Delete implements the delete of jenkins-pipeline-kubernetes. +func (p Plugin) Delete(options map[string]interface{}) (bool, error) { + return jenkinspipelinekubernetes.Delete(options) +} + +// Read implements the read of jenkins-pipeline-kubernetes. +func (p Plugin) Read(options map[string]interface{}) (map[string]interface{}, error) { + return jenkinspipelinekubernetes.Read(options) +} + +// DevStreamPlugin is the exported variable used by the DevStream core. +var DevStreamPlugin Plugin + +func main() { + log.Infof("%T: %s is a plugin for DevStream. Use it with DevStream.\n", DevStreamPlugin, NAME) +} diff --git a/docs/plugins/jenkins-pipeline-kubernetes.md b/docs/plugins/jenkins-pipeline-kubernetes.md new file mode 100644 index 000000000..58776403d --- /dev/null +++ b/docs/plugins/jenkins-pipeline-kubernetes.md @@ -0,0 +1,10 @@ +# jenkins-pipeline-kubernetes plugin + +TODO(aFlyBird0): Add your document here. +## Usage + +```yaml + +--8<-- "jenkins-pipeline-kubernetes.yaml" + +``` diff --git a/docs/plugins/jenkins-pipeline-kubernetes.zh.md b/docs/plugins/jenkins-pipeline-kubernetes.zh.md new file mode 100644 index 000000000..1b9d4a3d2 --- /dev/null +++ b/docs/plugins/jenkins-pipeline-kubernetes.zh.md @@ -0,0 +1,21 @@ +# jenkins-pipeline-kubernetes 插件 + +这个插件在已有的 Jenkins 上建立 Jenkins job, 将 GitHub 作为 SCM。 + +步骤: + +1. 访问 Jenkins web UI,创建 token。步骤:People -> admin ->Configure -> API Token -> Add new Token。 +2. 按需修改配置项,其中 `githubRepoUrl` 为 GitHub 仓库地址,应预先建立一个 GitHub 仓库,并创建一个名为 "Jenkinsfile" 的文件放至仓库根目录。 +3. 设置环境变量 + - `GITHUB_TOKEN` + - `JENKINS_TOKEN` + +## 用例 + +```yaml + +--8<-- "jenkins-pipeline-kubernetes.yaml" + +``` + +目前,所有选项均为必填项。 diff --git a/docs/plugins/jenkins.md b/docs/plugins/jenkins.md index 8efb4837e..1de434ddf 100644 --- a/docs/plugins/jenkins.md +++ b/docs/plugins/jenkins.md @@ -2,6 +2,8 @@ This plugin installs [Jenkins](https://jenkins.io) in an existing Kubernetes cluster using the Helm chart. +It also installs [GitHub Pull Request Builder(ghprb)](https://plugins.jenkins.io/ghprb/) and [OWASP Markup Formatter](https://plugins.jenkins.io/antisamy-markup-formatter/) plugins. Then enable HTML parsing using OWASP Markup Formatter Plugin , useful with ghprb plugin. + ## Usage ### Production Environment diff --git a/docs/plugins/jenkins.zh.md b/docs/plugins/jenkins.zh.md index 3d8b60344..6c685b514 100644 --- a/docs/plugins/jenkins.zh.md +++ b/docs/plugins/jenkins.zh.md @@ -2,6 +2,8 @@ 本插件使用 helm 在已有的 k8s 集群上安装 [Jenkins](https://jenkins.io)。 +并且安装 [GitHub Pull Request Builder](https://plugins.jenkins.io/ghprb/) 插件和 [OWASP Markup Formatter](https://plugins.jenkins.io/antisamy-markup-formatter/) 插件;同时利用 OWASP Markup Formatter 插件激活 HTML 渲染模式。 + ## 使用方法 ### 生产环境 diff --git a/go.mod b/go.mod index 0fdac543f..1a7c66dbc 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/mittwald/go-helm-client v0.8.4 github.com/onsi/ginkgo/v2 v2.1.4 github.com/onsi/gomega v1.19.0 + github.com/parnurzeal/gorequest v0.2.16 github.com/sirupsen/logrus v1.8.1 github.com/spf13/cobra v1.4.0 github.com/spf13/viper v1.8.1 @@ -207,6 +208,7 @@ require ( k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/kubectl v0.22.4 // indirect k8s.io/kubernetes v1.22.4 // indirect + moul.io/http2curl v1.0.0 // indirect oras.land/oras-go v0.4.0 // indirect sigs.k8s.io/kustomize/api v0.8.11 // indirect sigs.k8s.io/kustomize/kyaml v0.11.0 // indirect diff --git a/go.sum b/go.sum index a0e629812..d195173e8 100644 --- a/go.sum +++ b/go.sum @@ -1057,6 +1057,8 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= +github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= @@ -1979,6 +1981,8 @@ modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= oras.land/oras-go v0.4.0 h1:u6+7D+raZDYHwlz/uOwNANiRmyYDSSMW7A9E1xXycUQ= oras.land/oras-go v0.4.0/go.mod h1:VJcU+VE4rkclUbum5C0O7deEZbBYnsnpbGSACwTjOcg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/client.go b/internal/pkg/plugin/jenkinspipelinekubernetes/client.go new file mode 100644 index 000000000..2a537517e --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/client.go @@ -0,0 +1,153 @@ +package jenkinspipelinekubernetes + +import ( + _ "embed" + "fmt" + "os/exec" + "strings" + + "github.com/parnurzeal/gorequest" + + "github.com/devstream-io/devstream/pkg/util/log" +) + +// ClientInf represents the client abstraction for jenkins +type ClientInf interface { + CreateCredentialSecretText() error + CreateCredentialUsernamePassword() error + GetCrumb() (string, error) + GetCrumbHeader() (headerKey, headerValue string, err error) + CreateItem(jobXmlContent string) error +} + +// Client is the client for jenkins +type Client struct { + Opts *Options +} + +func NewClient(options *Options) *Client { + return &Client{ + Opts: options, + } +} + +//func (c *Client) ifCredentialExists() bool { +// // todo +// return false +//} + +// CreateCredentialSecretText creates a credential in the type of "Secret text" +func (c *Client) CreateCredentialSecretText() error { + + accessURL := c.Opts.GetJenkinsAccessURL() + crumb, err := c.GetCrumb() + if err != nil { + return fmt.Errorf("failed to create credential secret: %s", err) + } + + // TODO(aFlyBird0): use gorequest to do the request + cmdCreateCredential := fmt.Sprintf(` +curl -H %s -X POST '%s/credentials/store/system/domain/_/createCredentials' \ +--data-urlencode 'json={ + "": "0", + "credentials": { + "scope": "GLOBAL", + "id": "%s", + "secret": "%s", + "description": "%s", + "$class": "org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl" + } +}'`, crumb, accessURL, jenkinsCredentialID, c.Opts.GitHubToken, jenkinsCredentialDesc) + + cmd := exec.Command("sh", "-c", cmdCreateCredential) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create credential secret: %s", err) + } + + return nil +} + +// CreateCredentialUsernamePassword creates a credential in the type of "Username with password" +func (c *Client) CreateCredentialUsernamePassword() error { + + accessURL := c.Opts.GetJenkinsAccessURL() + crumb, err := c.GetCrumb() + if err != nil { + return fmt.Errorf("failed to create credential secret: %s", err) + } + + // TODO(aFlyBird0): use gorequest to do the request + cmdCreateCredential := fmt.Sprintf(` +curl -H %s -X POST '%s/credentials/store/system/domain/_/createCredentials' \ +--data-urlencode 'json={ + "": "0", + "credentials": { + "scope": "GLOBAL", + "id": "%s", + "username": "foo-useless-username", + "password": "%s", + "description": "%s", + "$class": "com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl" + } +}'`, crumb, accessURL, jenkinsCredentialID, c.Opts.GitHubToken, jenkinsCredentialDesc) + + cmd := exec.Command("sh", "-c", cmdCreateCredential) + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to create credential secret: %s", err) + } + + return nil +} + +// GetCrumb returns the crumb for jenkins, +// jenkins uses crumb to prevent CSRF(cross-site request forgery), +// format: "Jenkins-Crumb:a70290b6423777f0a4c771d4805637ac36d5fd78336a20d48d72167ef5f13b9a" +// ref: https://www.jenkins.io/doc/upgrade-guide/2.176/#upgrading-to-jenkins-lts-2-176-3 +// ref: https://stackoverflow.com/questions/44711696/jenkins-403-no-valid-crumb-was-included-in-the-request +func (c *Client) GetCrumb() (string, error) { + request := gorequest.New() + getCrumbURL := c.Opts.GetJenkinsAccessURL() + `/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)` + resp, body, errs := request.Get(getCrumbURL).End() + log.Debugf("GetCrumb url: %s", getCrumbURL) + if len(errs) != 0 { + return "", fmt.Errorf("failed to get crumb: %s", errs) + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("failed to get crumb, here is response: %s", body) + } + + return strings.TrimSpace(body), nil +} + +// GetCrumbHeader behaves like GetCrumb, but it returns the header key and value +func (c *Client) GetCrumbHeader() (headerKey, headerValue string, err error) { + // crumb format: "Jenkins-Crumb:a70290b6423777f0a4c771d4805637ac36d5fd78336a20d48d72167ef5f13b9a" + crumb, err := c.GetCrumb() + if err != nil { + return "", "", err + } + crumbMap := strings.Split(crumb, ":") + if len(crumbMap) != 2 { + return "", "", fmt.Errorf("failed to get crumb, here is response: %s", crumb) + } + return crumbMap[0], crumbMap[1], nil +} + +//go:embed job-template.xml +var jobTemplate string + +// CreateItem creates a job in jenkins with the given job xml +func (c *Client) CreateItem(jobXmlContent string) error { + request := gorequest.New() + resp, body, errs := request.Post(c.Opts.GetJenkinsAccessURL()+"/createItem"). + Set("Content-Type", "application/xml"). + Query("name=" + c.Opts.JenkinsJobName).Send(jobXmlContent).End() + + if len(errs) != 0 { + return fmt.Errorf("failed to create job: %s", errs) + } else if resp.StatusCode != 200 { + return fmt.Errorf("failed to create job, here is response: %s", body) + } + + return nil +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/create.go b/internal/pkg/plugin/jenkinspipelinekubernetes/create.go new file mode 100644 index 000000000..505a0346e --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/create.go @@ -0,0 +1,98 @@ +package jenkinspipelinekubernetes + +import ( + _ "embed" + "fmt" + "strings" + + "github.com/mitchellh/mapstructure" + + "github.com/devstream-io/devstream/pkg/util/log" +) + +const ( + jenkinsCredentialID = "credential-jenkins-pipeline-kubernetes-by-devstream" + jenkinsCredentialDesc = "Jenkins Pipeline secret, created by devstream/jenkins-pipeline-kubernetes" +) + +func Create(options map[string]interface{}) (map[string]interface{}, error) { + var opts Options + if err := mapstructure.Decode(options, &opts); err != nil { + return nil, err + } + + if errs := validate(&opts); len(errs) != 0 { + for _, e := range errs { + log.Errorf("Options error: %s.", e) + } + return nil, fmt.Errorf("opts are illegal") + } + + client := NewClient(&opts) + + // always try to create credential, there will be no error if it already exists + if err := client.CreateCredentialUsernamePassword(); err != nil { + return nil, err + } + + jobXmlContent := renderJobXml(jobTemplate, &opts) + + // TODO(aFlyBird0): check if the job already exists + + if err := client.CreateItem(jobXmlContent); err != nil { + return nil, fmt.Errorf("failed to create job: %s", err) + } + + // TODO(aFlyBird0): use JCasC to configure job creation + // configuration as code: update jenkins config + // + configure system -> GitHub Pull Request Builder -> Jenkins URL override + // + configure system -> GitHub Pull Request Builder -> git personal access token + // - job creation: + // + pr builder + // + main builder + // + GitHub project Project url + // + select GitHub Pull Request Builder under Build Triggers + // + Trigger phrase: optional + // + Pipeline Definition: pipeline script from SCM + // + Branch Specifier & Refspec: PR/main + // + unselect Lightweight checkout + // + Jenkinsfile template -> github repo (https://github.com/IronCore864/jenkins-test/blob/main/Jenkinsfile, https://plugins.jenkins.io/kubernetes/) + + // TODO(aFlyBird0): about how to create an new config yaml in JCasC: + // refer: https://plugins.jenkins.io/configuration-as-code/ + // refer: https://github.com/jenkinsci/helm-charts/blob/e4242561e5ea205bfa3405064cf5fe39b5a22d93/charts/jenkins/values.yaml#L341 + // refer: https://github.com/jenkinsci/helm-charts/blob/e4242561e5ea205bfa3405064cf5fe39b5a22d93/charts/jenkins/templates/jcasc-config.yaml#L1-L25 + // example: If we want to create a new config yaml in JCasC, we can use the following code: + // 1. render this ConfigMap by yourself: https://github.com/jenkinsci/helm-charts/blob/e4242561e5ea205bfa3405064cf5fe39b5a22d93/charts/jenkins/templates/jcasc-config.yaml#L6-L26 + // 2. $key should be your config file name(without .yaml extension), value should be the real config file content + // 3. once you want to put a new config yaml into $JenkinsHome/config-as-code/ folder, just use k8s sdk to create a new ConfigMap + // 4. don't forget to add a label to the ConfigMap, such as "author: devstream". So that we could easy to filter the ConfigMap created by devstream to delete them. + // 5. if you want to update an existing config yaml, just use k8s sdk to update the ConfigMap + // 6. it seems that once you create/update a ConfigMap, Jenkins(installed by helm) will automatically reload the config yaml, + // if not, you can use the following api to reload the config yaml: "POST JENKINS_URL/configuration-as-code/reload"( remember to add jenkins crumb in http header) + // see here for info:https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/features/configurationReload.md + // there are many things to do: + // 1. figure out the JCasC content we want to create + // 2. create a new ConfigMap according to the explanation above + // 3. handle update of ConfigMap + // 4. add "read" functions to the ConfigMap to get the content of the ConfigMap and check if the resource is drifted + // 5. maybe we also should consider to expose some config key in ConfigMap to the user + + // TODO(aFlyBird0): build dtm resource + + // TODO(aFlyBird0): what if user doesn't use helm to install jenkins? then the JCasC may not be automatically reloaded. + + return (&resource{}).toMap(), nil +} + +// TODO(aFlyBird0): unit test +// TODO(aFlyBird0): now jenkins script path is hardcoded to "Jenkinsfile", it should be configurable +func renderJobXml(jobTemplate string, opts *Options) string { + // note: maybe it is better to use html/template to generate the job template, + // but that way is complex and this is the simplest way to do it + jobXml := strings.Replace(jobTemplate, "{{.GitHubRepoURL}}", opts.GitHubRepoURL, 1) + jobXml = strings.Replace(jobXml, "{{.CredentialsID}}", jenkinsCredentialID, 1) + + log.Debugf("job xml rendered: %s", jobXml) + return jobXml +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/delete.go b/internal/pkg/plugin/jenkinspipelinekubernetes/delete.go new file mode 100644 index 000000000..49d3a1910 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/delete.go @@ -0,0 +1,29 @@ +package jenkinspipelinekubernetes + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + + "github.com/devstream-io/devstream/pkg/util/log" +) + +func Delete(options map[string]interface{}) (bool, error) { + var opts Options + if err := mapstructure.Decode(options, &opts); err != nil { + return false, err + } + + if errs := validate(&opts); len(errs) != 0 { + for _, e := range errs { + log.Errorf("Options error: %s.", e) + } + return false, fmt.Errorf("opts are illegal") + } + + // TODO(aFlyBird0): use helm uninstall to delete the resource created by devstream + // TODO(aFlyBird0): filter ConfigMaps Created by devstream in the "jenkins" namespace and delete them(filter by label) + // TODO(aFlyBird0): delete other resources created by devstream(if exists) + + return true, nil +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes.go b/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes.go new file mode 100644 index 000000000..7dafaac88 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes.go @@ -0,0 +1,9 @@ +package jenkinspipelinekubernetes + +// TODO(aFlyBird0): specify the resource fields here. +type resource struct { +} + +func (res *resource) toMap() map[string]interface{} { + return map[string]interface{}{} +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes_suite_test.go b/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes_suite_test.go new file mode 100644 index 000000000..0b0c314a0 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/jenkinspipelinekubernetes_suite_test.go @@ -0,0 +1,13 @@ +package jenkinspipelinekubernetes_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestJenkinspipelinekubernetes(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Jenkinspipelinekubernetes Suite") +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/job-template.xml b/internal/pkg/plugin/jenkinspipelinekubernetes/job-template.xml new file mode 100644 index 000000000..3fcf24ee2 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/job-template.xml @@ -0,0 +1,29 @@ + + + + false + + + + 2 + + + {{.GitHubRepoURL}} + {{.CredentialsID}} + + + + + */main + + + false + + + + Jenkinsfile + false + + + false + diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/options.go b/internal/pkg/plugin/jenkinspipelinekubernetes/options.go new file mode 100644 index 000000000..e61878fa0 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/options.go @@ -0,0 +1,18 @@ +package jenkinspipelinekubernetes + +import "fmt" + +// Options is the struct for configurations of the jenkins-pipeline-kubernetes plugin. +type Options struct { + JenkinsURL string `mapstructure:"jenkinsUrl" validate:"required,hostname_port"` + JenkinsUser string `mapstructure:"jenkinsUser" validate:"required"` + JenkinsToken string `mapstructure:"jenkinsToken"` + GitHubToken string `mapstructure:"githubToken"` + GitHubRepoURL string `mapstructure:"githubRepoUrl" validate:"required"` + JenkinsJobName string `mapstructure:"jenkinsJobName" validate:"required"` + // TODO(aFlyBird0): add options to configure the script path in GitHub repo, now it is hardcoded to "Jenkinsfile" +} + +func (options *Options) GetJenkinsAccessURL() string { + return fmt.Sprintf("http://%s:%s@%s", options.JenkinsUser, options.JenkinsToken, options.JenkinsURL) +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/read.go b/internal/pkg/plugin/jenkinspipelinekubernetes/read.go new file mode 100644 index 000000000..18ea8f638 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/read.go @@ -0,0 +1,32 @@ +package jenkinspipelinekubernetes + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + + "github.com/devstream-io/devstream/pkg/util/log" +) + +func Read(options map[string]interface{}) (map[string]interface{}, error) { + var opts Options + if err := mapstructure.Decode(options, &opts); err != nil { + return nil, err + } + + if errs := validate(&opts); len(errs) != 0 { + for _, e := range errs { + log.Errorf("Options error: %s.", e) + } + return nil, fmt.Errorf("opts are illegal") + } + + // TODO(aFlyBird0): specify the resource to be read and the way to read it, such as: + // plugins install info(GitHub Pull Request Builder Plugin and OWASP Markup Formatter must be installed) + // job list(filter the jobs which are created by devstream) + // credential list(filter the credentials which are created by devstream)(if credential created by devstream exists) + // JCasC configuration + // job configuration && status + + return nil, nil +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/update.go b/internal/pkg/plugin/jenkinspipelinekubernetes/update.go new file mode 100644 index 000000000..dfc7f07fd --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/update.go @@ -0,0 +1,30 @@ +package jenkinspipelinekubernetes + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + + "github.com/devstream-io/devstream/pkg/util/log" +) + +func Update(options map[string]interface{}) (map[string]interface{}, error) { + var opts Options + if err := mapstructure.Decode(options, &opts); err != nil { + return nil, err + } + + if errs := validate(&opts); len(errs) != 0 { + for _, e := range errs { + log.Errorf("Options error: %s.", e) + } + return nil, fmt.Errorf("opts are illegal") + } + + // TODO(aFlyBird0): determine how to update the resource, such as: + // if some config/resource are changed, we should restart the Jenkins + // some, we should only call some update function + // others, we just ignore them + + return (&resource{}).toMap(), nil +} diff --git a/internal/pkg/plugin/jenkinspipelinekubernetes/validate.go b/internal/pkg/plugin/jenkinspipelinekubernetes/validate.go new file mode 100644 index 000000000..e0c291737 --- /dev/null +++ b/internal/pkg/plugin/jenkinspipelinekubernetes/validate.go @@ -0,0 +1,45 @@ +package jenkinspipelinekubernetes + +import ( + "fmt" + "os" + "strings" + + "github.com/devstream-io/devstream/pkg/util/validator" +) + +// validate validates the options provided by the core. +func validate(options *Options) []error { + + // pre-handle options to remove "http://" from JenkinsURL + preHandleOptions(options) + + var retErrs []error + + if errs := validator.Struct(options); len(errs) != 0 { + retErrs = append(retErrs, errs...) + } + + options.GitHubToken = os.Getenv("GITHUB_TOKEN") + + // TODO(aFlyBird0): now jenkins token should be provided by the user, + // so, user should install jenkins first and stop to set the token in env, then install this pipeline plugin. + // could we generate a token automatically in "jenkins" plugin? + // and put it into .outputs of "jenkins" plugin, + // so that user could run "jenkins" and "jenkins-pipeline-kubernetes" in the same tool file.(using depends on). + options.JenkinsToken = os.Getenv("JENKINS_TOKEN") + if options.GitHubToken == "" { + retErrs = append(retErrs, fmt.Errorf("GITHUB_TOKEN is required")) + } + if options.JenkinsToken == "" { + retErrs = append(retErrs, fmt.Errorf("JENKINS_TOKEN is required")) + } + + // TODO(aFlyBird0): check if the jenkins url is valid (try to connect to jenkins) + + return retErrs +} + +func preHandleOptions(options *Options) { + options.JenkinsURL = strings.Replace(options.JenkinsURL, "http://", "", 1) +} diff --git a/internal/pkg/show/config/embed_gen.go b/internal/pkg/show/config/embed_gen.go index 5fb2e87c6..e80e1e214 100644 --- a/internal/pkg/show/config/embed_gen.go +++ b/internal/pkg/show/config/embed_gen.go @@ -48,6 +48,9 @@ var ( //go:embed plugins/helm-generic.yaml HelmGenericDefaultConfig string + //go:embed plugins/jenkins-pipeline-kubernetes.yaml + JenkinsPipelineKubernetesDefaultConfig string + //go:embed plugins/jenkins.yaml JenkinsDefaultConfig string @@ -87,6 +90,7 @@ var pluginDefaultConfigs = map[string]string{ "gitlabci-golang": GitlabciGolangDefaultConfig, "hashicorp-vault": HashicorpVaultDefaultConfig, "helm-generic": HelmGenericDefaultConfig, + "jenkins-pipeline-kubernetes": JenkinsPipelineKubernetesDefaultConfig, "jenkins": JenkinsDefaultConfig, "jira-github-integ": JiraGithubIntegDefaultConfig, "kube-prometheus": KubePrometheusDefaultConfig, diff --git a/internal/pkg/show/config/plugins/jenkins-pipeline-kubernetes.yaml b/internal/pkg/show/config/plugins/jenkins-pipeline-kubernetes.yaml new file mode 100644 index 000000000..df02d82bb --- /dev/null +++ b/internal/pkg/show/config/plugins/jenkins-pipeline-kubernetes.yaml @@ -0,0 +1,14 @@ +tools: +# name of the tool +- name: jenkins-pipeline-kubernetes + # id of the tool instance + instanceID: default + # format: name.instanceID; If specified, dtm will make sure the dependency is applied first before handling this tool. + dependsOn: [ ] + # options for the plugin + options: + # jenkinsUrl, format: hostname:port + jenkinsUrl: localhost:8080 + jenkinsUser: admin + githubRepoUrl: https://github.com/xxx/xxx.git + jenkinsJobName: diff --git a/internal/pkg/show/config/plugins/jenkins.yaml b/internal/pkg/show/config/plugins/jenkins.yaml index d84f41f45..e7740e66f 100644 --- a/internal/pkg/show/config/plugins/jenkins.yaml +++ b/internal/pkg/show/config/plugins/jenkins.yaml @@ -44,3 +44,19 @@ tools: controller: serviceType: NodePort nodePort: 32000 + additionalPlugins: + # install "GitHub Pull Request Builder" plugin, see https://plugins.jenkins.io/ghprb/ for more details + - ghprb + # install "OWASP Markup Formatter" plugin, see https://plugins.jenkins.io/antisamy-markup-formatter/ for more details + - antisamy-markup-formatter + # Enable HTML parsing using OWASP Markup Formatter Plugin (antisamy-markup-formatter), useful with ghprb plugin. + enableRawHtmlMarkupFormatter: true + # Jenkins Configuraction as Code, refer to https://plugins.jenkins.io/configuration-as-code/ for more details + # notice: All configuration files that are discovered MUST be supplementary. They cannot overwrite each other's configuration values. This creates a conflict and raises a ConfiguratorException. + JCasC: + defaultConfig: true + # each key-value in configScripts will be added to the ${JENKINS_HOME}/casc_configs/ directory as a file. + configScripts: + # this will create a file named "safe_html.yaml" in the ${JENKINS_HOME}/casc_configs/ directory. + # it is used to configure the "Safe HTML" plugin. + # filename must meet RFC 1123, see https://tools.ietf.org/html/rfc1123 for more details