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  }