| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- package gitlab
- import (
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "github.com/porter-dev/porter/api/server/shared/commonutils"
- "github.com/porter-dev/porter/api/server/shared/config"
- "github.com/porter-dev/porter/internal/oauth"
- "github.com/porter-dev/porter/internal/repository"
- "github.com/xanzy/go-gitlab"
- "gopkg.in/yaml.v2"
- )
- type GitlabCI struct {
- ServerURL string
- GitRepoPath string
- GitBranch string
- Repo repository.Repository
- ProjectID uint
- ClusterID uint
- UserID uint
- IntegrationID uint
- PorterConf *config.Config
- ReleaseName string
- ReleaseNamespace string
- FolderPath string
- PorterToken string
- defaultGitBranch string
- pID string
- gitlabInstanceURL string
- }
- func (g *GitlabCI) Setup() error {
- client, err := g.getClient()
- if err != nil {
- return err
- }
- g.pID = g.GitRepoPath
- err = g.setGitlabDefaultBranch(client)
- if err != nil {
- return err
- }
- err = g.createGitlabSecret(client)
- if err != nil {
- return err
- }
- jobName := getGitlabStageJobName(g.ReleaseName)
- ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
- Ref: gitlab.String(g.GitBranch),
- })
- if resp.StatusCode == http.StatusNotFound {
- // create .gitlab-ci.yml
- contentsMap := make(map[string]interface{})
- contentsMap["stages"] = []string{
- jobName,
- }
- contentsMap[jobName] = g.getCIJob(jobName)
- contentsYAML, _ := yaml.Marshal(contentsMap)
- _, _, err = client.RepositoryFiles.CreateFile(g.pID, ".gitlab-ci.yml", &gitlab.CreateFileOptions{
- Branch: gitlab.String(g.GitBranch),
- AuthorName: gitlab.String("Porter Bot"),
- AuthorEmail: gitlab.String("contact@getporter.dev"),
- Content: gitlab.String(string(contentsYAML)),
- CommitMessage: gitlab.String("Create .gitlab-ci.yml file"),
- })
- if err != nil {
- return fmt.Errorf("error creating .gitlab-ci.yml file: %w", err)
- }
- } else if err != nil {
- return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
- } else {
- // update .gitlab-ci.yml if needed
- // to preserve the order of the YAML, we use a MapSlice
- ciFileContentsMap := yaml.MapSlice{}
- err = yaml.Unmarshal(ciFile, &ciFileContentsMap)
- if err != nil {
- return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
- }
- var stagesInt []interface{}
- stagesIdx := -1
- for idx, elem := range ciFileContentsMap {
- if key, ok := elem.Key.(string); ok {
- if key == "stages" {
- stages, ok := elem.Value.([]interface{})
- if !ok {
- return fmt.Errorf("error converting stages to interface slice")
- }
- stagesInt = stages
- stagesIdx = idx
- break
- }
- } else {
- return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
- }
- }
- // two cases can happen here:
- // 1: "stages" exists
- // 2: "stages" does not exist
- if stagesIdx >= 0 { // 1: "stages" exists
- stageExists := false
- for _, stage := range stagesInt {
- stageStr, ok := stage.(string)
- if !ok {
- return fmt.Errorf("error converting from interface to string")
- }
- if stageStr == jobName {
- stageExists = true
- break
- }
- }
- if !stageExists {
- stagesInt = append(stagesInt, jobName)
- ciFileContentsMap[stagesIdx] = yaml.MapItem{
- Key: "stages",
- Value: stagesInt,
- }
- }
- } else { // 2: "stages" does not exist
- stagesInt = append(stagesInt, jobName)
- ciFileContentsMap = append(ciFileContentsMap, yaml.MapItem{
- Key: "stages",
- Value: stagesInt,
- })
- }
- ciFileContentsMap = append(ciFileContentsMap, yaml.MapItem{
- Key: jobName,
- Value: g.getCIJob(jobName),
- })
- contentsYAML, err := yaml.Marshal(ciFileContentsMap)
- if err != nil {
- return fmt.Errorf("error marshalling contents of .gitlab-ci.yml while updating to add porter job")
- }
- _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
- Branch: gitlab.String(g.GitBranch),
- AuthorName: gitlab.String("Porter Bot"),
- AuthorEmail: gitlab.String("contact@getporter.dev"),
- Content: gitlab.String(string(contentsYAML)),
- CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
- })
- if err != nil {
- return fmt.Errorf("error updating .gitlab-ci.yml file to add porter job: %w", err)
- }
- }
- return nil
- }
- func (g *GitlabCI) Cleanup() error {
- client, err := g.getClient()
- if err != nil {
- return err
- }
- g.pID = g.GitRepoPath
- err = g.setGitlabDefaultBranch(client)
- if err != nil {
- return err
- }
- err = g.deleteGitlabSecret(client)
- if err != nil {
- return err
- }
- jobName := getGitlabStageJobName(g.ReleaseName)
- ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
- Ref: gitlab.String(g.GitBranch),
- })
- if resp.StatusCode == http.StatusNotFound {
- return nil
- } else if err != nil {
- return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
- }
- ciFileContentsMap := yaml.MapSlice{}
- err = yaml.Unmarshal(ciFile, &ciFileContentsMap)
- if err != nil {
- return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
- }
- var stagesInt []interface{}
- stagesIdx := -1
- for idx, elem := range ciFileContentsMap {
- if key, ok := elem.Key.(string); ok {
- if key == "stages" {
- stages, ok := elem.Value.([]interface{})
- if !ok {
- return fmt.Errorf("error converting stages to interface slice")
- }
- stagesInt = stages
- stagesIdx = idx
- break
- }
- } else {
- return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
- }
- }
- if stagesIdx >= 0 { // "stages" exists
- var newStages []string
- for _, stage := range stagesInt {
- stageStr, ok := stage.(string)
- if !ok {
- return fmt.Errorf("error converting from interface to string")
- }
- if stageStr != jobName {
- newStages = append(newStages, stageStr)
- }
- }
- ciFileContentsMap[stagesIdx] = yaml.MapItem{
- Key: "stages",
- Value: newStages,
- }
- }
- newCIFileContentsMap := yaml.MapSlice{}
- for _, elem := range ciFileContentsMap {
- if key, ok := elem.Key.(string); ok {
- if key != jobName {
- newCIFileContentsMap = append(newCIFileContentsMap, elem)
- }
- } else {
- return fmt.Errorf("invalid key '%v' in .gitlab-ci.yml", elem.Key)
- }
- }
- contentsYAML, err := yaml.Marshal(newCIFileContentsMap)
- if err != nil {
- return fmt.Errorf("error unmarshalling contents of .gitlab-ci.yml while updating to remove porter job")
- }
- _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
- Branch: gitlab.String(g.GitBranch),
- AuthorName: gitlab.String("Porter Bot"),
- AuthorEmail: gitlab.String("contact@getporter.dev"),
- Content: gitlab.String(string(contentsYAML)),
- CommitMessage: gitlab.String("Update .gitlab-ci.yml file"),
- })
- if err != nil {
- return fmt.Errorf("error updating .gitlab-ci.yml file to remove porter job: %w", err)
- }
- return nil
- }
- func (g *GitlabCI) getClient() (*gitlab.Client, error) {
- gi, err := g.Repo.GitlabIntegration().ReadGitlabIntegration(g.ProjectID, g.IntegrationID)
- if err != nil {
- return nil, err
- }
- giOAuthInt, err := g.Repo.GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(g.UserID, g.ProjectID, g.IntegrationID)
- if err != nil {
- return nil, err
- }
- oauthInt, err := g.Repo.OAuthIntegration().ReadOAuthIntegration(g.ProjectID, giOAuthInt.OAuthIntegrationID)
- if err != nil {
- return nil, err
- }
- accessToken, _, err := oauth.GetAccessToken(
- oauthInt.SharedOAuthModel,
- commonutils.GetGitlabOAuthConf(g.PorterConf, gi),
- oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(g.ProjectID, giOAuthInt, g.Repo),
- )
- if err != nil {
- return nil, err
- }
- client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
- if err != nil {
- return nil, err
- }
- g.gitlabInstanceURL = gi.InstanceURL
- return client, nil
- }
- func (g *GitlabCI) getCIJob(jobName string) yaml.MapSlice {
- res := yaml.MapSlice{}
- url, _ := url.Parse(g.gitlabInstanceURL)
- res = append(res,
- yaml.MapItem{
- Key: "rules",
- Value: []map[string]string{
- {
- "if": fmt.Sprintf("$CI_COMMIT_BRANCH == \"%s\" && $CI_PIPELINE_SOURCE == \"push\"", g.GitBranch),
- },
- },
- },
- )
- if url.Hostname() == "gitlab.com" || url.Hostname() == "www.gitlab.com" {
- res = append(res,
- yaml.MapItem{
- Key: "image",
- Value: "docker:latest",
- },
- yaml.MapItem{
- Key: "services",
- Value: []string{
- "docker:dind",
- },
- },
- yaml.MapItem{
- Key: "script",
- Value: []string{
- fmt.Sprintf(
- "docker run --rm --workdir=\"/app\" "+
- "-v /var/run/docker.sock:/var/run/docker.sock "+
- "-v $(pwd):/app "+
- "public.ecr.aws/o1j4x7p4/porter-cli:latest "+
- "update --host \"%s\" --project %d --cluster %d "+
- "--token \"$%s\" --app \"%s\" "+
- "--tag \"$(echo $CI_COMMIT_SHA | cut -c1-7)\" --namespace \"%s\" --stream",
- g.ServerURL, g.ProjectID, g.ClusterID, g.getPorterTokenSecretName(),
- g.ReleaseName, g.ReleaseNamespace,
- ),
- },
- },
- yaml.MapItem{
- Key: "tags",
- Value: []string{
- "docker",
- },
- },
- )
- } else {
- res = append(res,
- yaml.MapItem{
- Key: "image",
- Value: map[string]interface{}{
- "name": "public.ecr.aws/o1j4x7p4/porter-cli:latest",
- "entrypoint": []string{
- "",
- },
- },
- },
- yaml.MapItem{
- Key: "script",
- Value: []string{
- fmt.Sprintf(
- "porter update --host \"%s\" --project %d --cluster %d "+
- "--token \"$%s\" --app \"%s\" "+
- "--tag \"$(echo $CI_COMMIT_SHA | cut -c1-7)\" --namespace \"%s\" --stream",
- g.ServerURL, g.ProjectID, g.ClusterID, g.getPorterTokenSecretName(),
- g.ReleaseName, g.ReleaseNamespace,
- ),
- },
- },
- yaml.MapItem{
- Key: "tags",
- Value: []string{
- "porter-runner",
- },
- },
- )
- }
- res = append(res,
- yaml.MapItem{
- Key: "stage",
- Value: jobName,
- },
- yaml.MapItem{
- Key: "timeout",
- Value: "20 minutes",
- },
- yaml.MapItem{
- Key: "variables",
- Value: map[string]string{
- "GIT_STRATEGY": "clone",
- },
- },
- )
- return res
- }
- func (g *GitlabCI) createGitlabSecret(client *gitlab.Client) error {
- _, resp, err := client.ProjectVariables.GetVariable(g.pID, g.getPorterTokenSecretName(),
- &gitlab.GetProjectVariableOptions{})
- if resp.StatusCode == http.StatusNotFound {
- _, _, err = client.ProjectVariables.CreateVariable(g.pID, &gitlab.CreateProjectVariableOptions{
- Key: gitlab.String(g.getPorterTokenSecretName()),
- Value: gitlab.String(g.PorterToken),
- Masked: gitlab.Bool(true),
- })
- if err != nil {
- return fmt.Errorf("error creating porter token variable: %w", err)
- }
- return nil
- } else if err != nil {
- return fmt.Errorf("error getting porter token variable: %w", err)
- }
- _, _, err = client.ProjectVariables.UpdateVariable(g.pID, g.getPorterTokenSecretName(),
- &gitlab.UpdateProjectVariableOptions{
- Value: gitlab.String(g.PorterToken),
- Masked: gitlab.Bool(true),
- },
- )
- if err != nil {
- return fmt.Errorf("error updating porter token variable: %w", err)
- }
- return nil
- }
- func (g *GitlabCI) deleteGitlabSecret(client *gitlab.Client) error {
- _, err := client.ProjectVariables.RemoveVariable(g.pID, g.getPorterTokenSecretName(),
- &gitlab.RemoveProjectVariableOptions{})
- if err != nil {
- return fmt.Errorf("error removing porter token variable: %w", err)
- }
- return nil
- }
- func (g *GitlabCI) getPorterTokenSecretName() string {
- return fmt.Sprintf("PORTER_TOKEN_%d_%s", g.ProjectID, strings.ToLower(strings.ReplaceAll(g.ReleaseName, "-", "_")))
- }
- func getGitlabStageJobName(releaseName string) string {
- return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
- }
- func (g *GitlabCI) setGitlabDefaultBranch(client *gitlab.Client) error {
- opt := &gitlab.ListBranchesOptions{
- ListOptions: gitlab.ListOptions{
- PerPage: 20,
- Page: 1,
- },
- }
- for {
- branches, resp, err := client.Branches.ListBranches(g.pID, opt)
- if err != nil {
- return fmt.Errorf("error fetching list of branches: %w", err)
- }
- for _, branch := range branches {
- if branch.Default {
- g.defaultGitBranch = branch.Name
- return nil
- }
- }
- // Exit the loop when we've seen all pages.
- if resp.NextPage == 0 {
- break
- }
- // Update the page number to get the next page.
- opt.Page = resp.NextPage
- }
- return nil
- }
|