|
|
@@ -0,0 +1,303 @@
|
|
|
+package gitlab
|
|
|
+
|
|
|
+import (
|
|
|
+ "fmt"
|
|
|
+ "net/http"
|
|
|
+ "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
|
|
|
+ GitRepoName string
|
|
|
+ GitRepoOwner 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
|
|
|
+}
|
|
|
+
|
|
|
+func (g *GitlabCI) Setup() error {
|
|
|
+ client, err := g.getClient()
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
|
|
|
+
|
|
|
+ branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error fetching list of branches: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, branch := range branches {
|
|
|
+ if branch.Default {
|
|
|
+ g.defaultGitBranch = branch.Name
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ err = g.createGitlabSecret(client)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ jobName := getGitlabStageJobName(g.ReleaseName)
|
|
|
+
|
|
|
+ ciFile, resp, err := client.RepositoryFiles.GetFile(g.pID, ".gitlab-ci.yml", &gitlab.GetFileOptions{
|
|
|
+ Ref: gitlab.String(g.defaultGitBranch),
|
|
|
+ })
|
|
|
+
|
|
|
+ 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.defaultGitBranch),
|
|
|
+ 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
|
|
|
+ ciFileContentsMap := make(map[string]interface{})
|
|
|
+ err = yaml.Unmarshal([]byte(ciFile.Content), ciFileContentsMap)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ stages, ok := ciFileContentsMap["stages"].([]string)
|
|
|
+
|
|
|
+ if !ok {
|
|
|
+ return fmt.Errorf("error converting stages to string slice")
|
|
|
+ }
|
|
|
+
|
|
|
+ stageExists := false
|
|
|
+
|
|
|
+ for _, stage := range stages {
|
|
|
+ if stage == jobName {
|
|
|
+ stageExists = true
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !stageExists {
|
|
|
+ stages = append(stages, jobName)
|
|
|
+
|
|
|
+ ciFileContentsMap["stages"] = stages
|
|
|
+ }
|
|
|
+
|
|
|
+ ciFileContentsMap[jobName] = g.getCIJob(jobName)
|
|
|
+
|
|
|
+ contentsYAML, _ := yaml.Marshal(ciFileContentsMap)
|
|
|
+
|
|
|
+ _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
|
|
|
+ Branch: gitlab.String(g.defaultGitBranch),
|
|
|
+ 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 = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
|
|
|
+
|
|
|
+ branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error fetching list of branches: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, branch := range branches {
|
|
|
+ if branch.Default {
|
|
|
+ g.defaultGitBranch = branch.Name
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ err = g.deleteGitlabSecret(client)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ jobName := getGitlabStageJobName(g.ReleaseName)
|
|
|
+
|
|
|
+ ciFile, resp, err := client.RepositoryFiles.GetFile(g.pID, ".gitlab-ci.yml", &gitlab.GetFileOptions{
|
|
|
+ Ref: gitlab.String(g.defaultGitBranch),
|
|
|
+ })
|
|
|
+
|
|
|
+ if resp.StatusCode == http.StatusNotFound {
|
|
|
+ return nil
|
|
|
+ } else if err != nil {
|
|
|
+ return fmt.Errorf("error getting .gitlab-ci.yml file: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ ciFileContentsMap := make(map[string]interface{})
|
|
|
+ err = yaml.Unmarshal([]byte(ciFile.Content), ciFileContentsMap)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return fmt.Errorf("error unmarshalling existing .gitlab-ci.yml: %w", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ stages, ok := ciFileContentsMap["stages"].([]string)
|
|
|
+
|
|
|
+ if !ok {
|
|
|
+ return fmt.Errorf("error converting stages to string slice")
|
|
|
+ }
|
|
|
+
|
|
|
+ var newStages []string
|
|
|
+
|
|
|
+ for _, stage := range stages {
|
|
|
+ if stage != jobName {
|
|
|
+ newStages = append(newStages, stage)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ciFileContentsMap["stage"] = newStages
|
|
|
+
|
|
|
+ delete(ciFileContentsMap, jobName)
|
|
|
+
|
|
|
+ contentsYAML, _ := yaml.Marshal(ciFileContentsMap)
|
|
|
+
|
|
|
+ _, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
|
|
|
+ Branch: gitlab.String(g.defaultGitBranch),
|
|
|
+ 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
|
|
|
+ }
|
|
|
+
|
|
|
+ oauthInt, err := g.Repo.GitlabAppOAuthIntegration().ReadGitlabAppOAuthIntegration(g.UserID, g.ProjectID, g.IntegrationID)
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ accessToken, _, err := oauth.GetAccessToken(oauthInt.SharedOAuthModel, commonutils.GetGitlabOAuthConf(g.PorterConf, gi),
|
|
|
+ oauth.MakeUpdateGitlabAppOAuthIntegrationFunction(oauthInt, g.Repo))
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ client, err := gitlab.NewOAuthClient(accessToken, gitlab.WithBaseURL(gi.InstanceURL))
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return client, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (g *GitlabCI) getCIJob(jobName string) map[string]interface{} {
|
|
|
+ return map[string]interface{}{
|
|
|
+ "image": "public.ecr.aws/o1j4x7p4/porter-cli:latest",
|
|
|
+ "stage": jobName,
|
|
|
+ "timeout": "20 minutes",
|
|
|
+ "script": []string{
|
|
|
+ fmt.Sprintf("export PORTER_HOST=\"%s\"", g.ServerURL),
|
|
|
+ fmt.Sprintf("export PORTER_PROJECT=\"%d\"", g.ProjectID),
|
|
|
+ fmt.Sprintf("export PORTER_CLUSTER=\"%d\"", g.ClusterID),
|
|
|
+ fmt.Sprintf("export PORTER_TOKEN=\"$%s\"", g.getPorterTokenSecretName()),
|
|
|
+ "export PORTER_TAG=\"$(echo $CI_COMMIT_SHA | cut -c1-7)\"",
|
|
|
+ fmt.Sprintf("porter update --app \"%s\" --tag \"$PORTER_TAG\" --namespace \"%s\" --path \"%s\" --stream",
|
|
|
+ g.ReleaseName, g.ReleaseNamespace, g.FolderPath),
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (g *GitlabCI) createGitlabSecret(client *gitlab.Client) error {
|
|
|
+ _, _, 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
|
|
|
+}
|
|
|
+
|
|
|
+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", g.ProjectID)
|
|
|
+}
|
|
|
+
|
|
|
+func getGitlabStageJobName(releaseName string) string {
|
|
|
+ return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
|
|
|
+}
|