github.com/drone/go-convert@v0.0.0-20240307072510-6bd371c65e61/convert/jenkins/convert.go (about) 1 // Copyright 2022 Harness, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package jenkins converts Jenkins pipelines to Harness pipelines. 16 package jenkins 17 18 import ( 19 "bytes" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "net/url" 27 "os" 28 "strconv" 29 "strings" 30 "time" 31 32 "github.com/drone/go-convert/convert/drone" 33 "github.com/drone/go-convert/convert/github" 34 "github.com/drone/go-convert/convert/gitlab" 35 ) 36 37 // Converter converts a Drone pipeline to a Harness 38 // v1 pipeline. 39 type Converter struct { 40 format Format 41 kubeEnabled bool 42 kubeNamespace string 43 kubeConnector string 44 dockerhubConn string 45 debug bool 46 token string 47 attempts int 48 } 49 50 // New creates a new Converter that converts a Drone 51 // pipeline to a Harness v1 pipeline. 52 func New(options ...Option) *Converter { 53 d := new(Converter) 54 55 // loop through and apply the options. 56 for _, option := range options { 57 option(d) 58 } 59 60 // set the default kubernetes namespace. 61 if d.kubeNamespace == "" { 62 d.kubeNamespace = "default" 63 } 64 65 // set the runtime to kubernetes if the kubernetes 66 // connector is configured. 67 if d.kubeConnector != "" { 68 d.kubeEnabled = true 69 } 70 71 // set the minimum number of attempts 72 if d.attempts == 0 { 73 d.attempts = 1 74 } 75 76 return d 77 } 78 79 // Convert downgrades a v1 pipeline. 80 func (d *Converter) Convert(r io.Reader) ([]byte, error) { 81 b, err := ioutil.ReadAll(r) 82 if err != nil { 83 return nil, err 84 } 85 return d.ConvertBytes(b) 86 } 87 88 // ConvertString downgrades a v1 pipeline. 89 func (d *Converter) ConvertBytes(b []byte) ([]byte, error) { 90 return d.retry(b) 91 } 92 93 // ConvertString downgrades a v1 pipeline. 94 func (d *Converter) ConvertString(s string) ([]byte, error) { 95 return d.ConvertBytes([]byte(s)) 96 } 97 98 // ConvertFile downgrades a v1 pipeline. 99 func (d *Converter) ConvertFile(p string) ([]byte, error) { 100 f, err := os.Open(p) 101 if err != nil { 102 return nil, err 103 } 104 defer f.Close() 105 return d.Convert(f) 106 } 107 108 // retry attempts the conversion with a backoff 109 func (d *Converter) retry(src []byte) ([]byte, error) { 110 var out []byte 111 var err error 112 for i := 0; i < d.attempts; i++ { 113 // puase before retry 114 if i != 0 { 115 // print status for debug purposes 116 fmt.Fprintln(os.Stderr, "attempt failed") 117 fmt.Fprintln(os.Stderr, err) 118 // 10 seconds before retry 119 time.Sleep(time.Second * 10) 120 } 121 // attempt the conversion 122 if out, err = d.convert(src); err == nil { 123 break 124 } 125 } 126 return out, err 127 } 128 129 // convert converts a Drone pipeline to a Harness pipeline. 130 func (d *Converter) convert(src []byte) ([]byte, error) { 131 132 // gpt input 133 req := &request{ 134 Model: "gpt-3.5-turbo", 135 Messages: []*message{ 136 { 137 Role: "user", 138 Content: fmt.Sprintf("Convert this Jenkinsfile to a %s Yaml.\n\n```\n%s\n```\n", d.format.String(), []byte(src)), 139 }, 140 }, 141 } 142 143 // gpt output 144 res := new(response) 145 146 // marshal the input to json 147 err := d.do("https://api.openai.com/v1/chat/completions", "POST", req, res) 148 if err != nil { 149 return nil, err 150 } 151 152 if len(res.Choices) == 0 { 153 return nil, errors.New("chat gpt returned a response with zero choices. conversion not possible.") 154 } 155 156 // extract the message 157 code := extractCodeFence(res.Choices[0].Message.Content) 158 159 if d.format == FromDrone { 160 // convert the pipeline yaml from the drone 161 // format to the harness yaml format. 162 converter := drone.New( 163 drone.WithDockerhub(d.dockerhubConn), 164 drone.WithKubernetes(d.kubeConnector, d.kubeNamespace), 165 ) 166 pipeline, err := converter.ConvertString(code) 167 if err != nil && d.debug { 168 // dump data for debug mode 169 os.Stdout.WriteString("\n") 170 os.Stdout.WriteString("---") 171 os.Stdout.WriteString("\n") 172 os.Stdout.WriteString(res.Choices[0].Message.Content) 173 os.Stdout.WriteString("\n") 174 os.Stdout.WriteString("---") 175 os.Stdout.WriteString("\n") 176 os.Stdout.Write(pipeline) 177 os.Stdout.WriteString("\n") 178 os.Stdout.WriteString("---") 179 os.Stdout.WriteString("\n") 180 } 181 return pipeline, err 182 } 183 184 if d.format == FromGitlab { 185 // convert the pipeline yaml from the gitlab 186 // format to the harness yaml format. 187 converter := gitlab.New( 188 gitlab.WithDockerhub(d.dockerhubConn), 189 gitlab.WithKubernetes(d.kubeConnector, d.kubeNamespace), 190 ) 191 pipeline, err := converter.ConvertString(code) 192 if err != nil { 193 // dump data for debug mode 194 if err != nil && d.debug { 195 os.Stdout.WriteString("\n") 196 os.Stdout.WriteString("---") 197 os.Stdout.WriteString("\n") 198 os.Stdout.WriteString(res.Choices[0].Message.Content) 199 os.Stdout.WriteString("\n") 200 os.Stdout.WriteString("---") 201 os.Stdout.WriteString("\n") 202 os.Stdout.Write(pipeline) 203 os.Stdout.WriteString("\n") 204 os.Stdout.WriteString("---") 205 os.Stdout.WriteString("\n") 206 } 207 } 208 return pipeline, err 209 } 210 211 // convert the pipeline yaml from the github 212 // format to the harness yaml format. 213 converter := github.New( 214 github.WithDockerhub(d.dockerhubConn), 215 github.WithKubernetes(d.kubeConnector, d.kubeNamespace), 216 ) 217 pipeline, err := converter.ConvertString(code) 218 if err != nil { 219 // dump data for debug mode 220 if err != nil && d.debug { 221 os.Stdout.WriteString("\n") 222 os.Stdout.WriteString("---") 223 os.Stdout.WriteString("\n") 224 os.Stdout.WriteString(res.Choices[0].Message.Content) 225 os.Stdout.WriteString("\n") 226 os.Stdout.WriteString("---") 227 os.Stdout.WriteString("\n") 228 os.Stdout.Write(pipeline) 229 os.Stdout.WriteString("\n") 230 os.Stdout.WriteString("---") 231 os.Stdout.WriteString("\n") 232 } 233 } 234 235 return pipeline, err 236 } 237 238 func extractCodeFence(s string) string { 239 // trim space 240 s = strings.TrimSpace(s) 241 s = strings.TrimSuffix(s, "```") 242 // find and trim the code fence prefix 243 if _, c, ok := strings.Cut(s, "```"); ok { 244 s = c 245 // find and trim the code fence suffix 246 if c, _, ok := strings.Cut(s, "```"); ok { 247 s = c 248 } 249 } 250 return strings.TrimPrefix(s, "yaml") 251 } 252 253 // 254 // Chat GPT Client 255 // TODO move to separate package 256 // 257 258 type request struct { 259 Model string `json:"model"` 260 Messages []*message `json:"messages"` 261 } 262 263 type message struct { 264 Role string `json:"role"` 265 Content string `json:"content"` 266 } 267 268 type response struct { 269 ID string `json:"id"` 270 Object string `json:"object"` 271 Created int `json:"created"` 272 Model string `json:"model"` 273 Usage struct { 274 PromptTokens int `json:"prompt_tokens"` 275 CompletionTokens int `json:"completion_tokens"` 276 TotalTokens int `json:"total_tokens"` 277 } `json:"usage"` 278 Choices []struct { 279 Message struct { 280 Role string `json:"role"` 281 Content string `json:"content"` 282 } `json:"message"` 283 FinishReason string `json:"finish_reason"` 284 Index int `json:"index"` 285 } 286 } 287 288 // helper function to make an http request 289 func (d *Converter) do(rawurl, method string, in, out interface{}) error { 290 body, err := d.open(rawurl, method, in, out) 291 if err != nil { 292 return err 293 } 294 defer body.Close() 295 if out != nil { 296 return json.NewDecoder(body).Decode(out) 297 } 298 return nil 299 } 300 301 // helper function to open an http request 302 func (d *Converter) open(rawurl, method string, in, out interface{}) (io.ReadCloser, error) { 303 uri, err := url.Parse(rawurl) 304 if err != nil { 305 return nil, err 306 } 307 req, err := http.NewRequest(method, uri.String(), nil) 308 if err != nil { 309 return nil, err 310 } 311 if in != nil { 312 decoded, derr := json.Marshal(in) 313 if derr != nil { 314 return nil, derr 315 } 316 buf := bytes.NewBuffer(decoded) 317 req.Body = ioutil.NopCloser(buf) 318 req.ContentLength = int64(len(decoded)) 319 req.Header.Set("Content-Length", strconv.Itoa(len(decoded))) 320 req.Header.Set("Content-Type", "application/json") 321 } 322 req.Header.Set("Authorization", "Bearer "+d.token) 323 resp, err := http.DefaultClient.Do(req) 324 if err != nil { 325 return nil, err 326 } 327 if resp.StatusCode > 299 { 328 defer resp.Body.Close() 329 out, _ := ioutil.ReadAll(resp.Body) 330 return nil, fmt.Errorf("client error %d: %s", resp.StatusCode, string(out)) 331 } 332 return resp.Body, nil 333 }