Browse Source

gitlab runners initial

Mohammed Nafees 4 năm trước cách đây
mục cha
commit
0ae101be7b

+ 72 - 47
api/server/handlers/release/create.go

@@ -18,6 +18,7 @@ import (
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/helm/loader"
 	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/registry"
@@ -160,13 +161,13 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		}
 	}
 
-	if request.GithubActionConfig != nil {
+	if request.GitActionConfig != nil {
 		_, _, err := createGitAction(
 			c.Config(),
 			user.ID,
 			cluster.ProjectID,
 			cluster.ID,
-			request.GithubActionConfig,
+			request.GitActionConfig,
 			request.Name,
 			namespace,
 			release,
@@ -288,56 +289,80 @@ func createGitAction(
 		return nil, nil, err
 	}
 
-	// create the commit in the git repo
-	gaRunner := &actions.GithubActions{
-		InstanceName:           config.ServerConf.InstanceName,
-		ServerURL:              config.ServerConf.ServerURL,
-		GithubOAuthIntegration: nil,
-		GithubAppID:            config.GithubAppConf.AppID,
-		GithubAppSecretPath:    config.GithubAppConf.SecretPath,
-		GithubInstallationID:   request.GitRepoID,
-		GitRepoName:            repoSplit[1],
-		GitRepoOwner:           repoSplit[0],
-		Repo:                   config.Repo,
-		ProjectID:              projectID,
-		ClusterID:              clusterID,
-		ReleaseName:            name,
-		ReleaseNamespace:       namespace,
-		GitBranch:              request.GitBranch,
-		DockerFilePath:         request.DockerfilePath,
-		FolderPath:             request.FolderPath,
-		ImageRepoURL:           request.ImageRepoURI,
-		PorterToken:            encoded,
-		Version:                "v0.1.0",
-		ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
-		DryRun:                 release == nil,
-	}
-
-	// Save the github err for after creating the git action config. However, we
-	// need to call Setup() in order to get the workflow file before writing the
-	// action config, in the case of a dry run, since the dry run does not create
-	// a git action config.
-	workflowYAML, githubErr := gaRunner.Setup()
+	var workflowYAML []byte
+	var gitErr error
+
+	if request.GitlabIntegrationID != 0 {
+		giRunner := &gitlab.GitlabCI{
+			ServerURL:        config.ServerConf.ServerURL,
+			GitRepoOwner:     repoSplit[0],
+			GitRepoName:      repoSplit[1],
+			Repo:             config.Repo,
+			ProjectID:        projectID,
+			ClusterID:        clusterID,
+			UserID:           userID,
+			IntegrationID:    request.GitlabIntegrationID,
+			PorterConf:       config,
+			ReleaseName:      name,
+			ReleaseNamespace: namespace,
+			FolderPath:       request.FolderPath,
+			PorterToken:      encoded,
+		}
 
-	if gaRunner.DryRun {
-		if githubErr != nil {
-			return nil, nil, githubErr
+		gitErr = giRunner.Setup()
+	} else {
+		// create the commit in the git repo
+		gaRunner := &actions.GithubActions{
+			InstanceName:           config.ServerConf.InstanceName,
+			ServerURL:              config.ServerConf.ServerURL,
+			GithubOAuthIntegration: nil,
+			GithubAppID:            config.GithubAppConf.AppID,
+			GithubAppSecretPath:    config.GithubAppConf.SecretPath,
+			GithubInstallationID:   request.GitRepoID,
+			GitRepoName:            repoSplit[1],
+			GitRepoOwner:           repoSplit[0],
+			Repo:                   config.Repo,
+			ProjectID:              projectID,
+			ClusterID:              clusterID,
+			ReleaseName:            name,
+			ReleaseNamespace:       namespace,
+			GitBranch:              request.GitBranch,
+			DockerFilePath:         request.DockerfilePath,
+			FolderPath:             request.FolderPath,
+			ImageRepoURL:           request.ImageRepoURI,
+			PorterToken:            encoded,
+			Version:                "v0.1.0",
+			ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
+			DryRun:                 release == nil,
 		}
 
-		return nil, workflowYAML, nil
+		// Save the github err for after creating the git action config. However, we
+		// need to call Setup() in order to get the workflow file before writing the
+		// action config, in the case of a dry run, since the dry run does not create
+		// a git action config.
+		workflowYAML, githubErr := gaRunner.Setup()
+
+		if gaRunner.DryRun {
+			if githubErr != nil {
+				return nil, nil, githubErr
+			}
+
+			return nil, workflowYAML, nil
+		}
 	}
 
 	// handle write to the database
 	ga, err := config.Repo.GitActionConfig().CreateGitActionConfig(&models.GitActionConfig{
-		ReleaseID:      release.ID,
-		GitRepo:        request.GitRepo,
-		GitBranch:      request.GitBranch,
-		ImageRepoURI:   request.ImageRepoURI,
-		GitRepoID:      request.GitRepoID,
-		DockerfilePath: request.DockerfilePath,
-		FolderPath:     request.FolderPath,
-		IsInstallation: true,
-		Version:        "v0.1.0",
+		ReleaseID:           release.ID,
+		GitRepo:             request.GitRepo,
+		GitBranch:           request.GitBranch,
+		ImageRepoURI:        request.ImageRepoURI,
+		GitRepoID:           request.GitRepoID,
+		GitlabIntegrationID: request.GitlabIntegrationID,
+		DockerfilePath:      request.DockerfilePath,
+		FolderPath:          request.FolderPath,
+		IsInstallation:      true,
+		Version:             "v0.1.0",
 	})
 
 	if err != nil {
@@ -353,8 +378,8 @@ func createGitAction(
 		return nil, nil, err
 	}
 
-	if githubErr != nil {
-		return nil, nil, githubErr
+	if gitErr != nil {
+		return nil, nil, gitErr
 	}
 
 	return ga.ToGitActionConfigType(), workflowYAML, nil

+ 53 - 20
api/server/handlers/release/delete.go

@@ -1,7 +1,9 @@
 package release
 
 import (
+	"fmt"
 	"net/http"
+	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -9,6 +11,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
 	"github.com/porter-dev/porter/internal/models"
 	"helm.sh/helm/v3/pkg/release"
 )
@@ -56,28 +59,58 @@ func (c *DeleteReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 			gitAction := rel.GitActionConfig
 
 			if gitAction != nil && gitAction.ID != 0 {
-				gaRunner, err := getGARunner(
-					c.Config(),
-					user.ID,
-					cluster.ProjectID,
-					cluster.ID,
-					rel.GitActionConfig,
-					helmRelease.Name,
-					helmRelease.Namespace,
-					rel,
-					helmRelease,
-				)
-
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-					return
-				}
+				if gitAction.GitlabIntegrationID != 0 {
+					repoSplit := strings.Split(gitAction.GitRepo, "/")
+
+					if len(repoSplit) != 2 {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("invalid formatting of repo name")))
+						return
+					}
+
+					giRunner := &gitlab.GitlabCI{
+						ServerURL:        c.Config().ServerConf.ServerURL,
+						GitRepoOwner:     repoSplit[0],
+						GitRepoName:      repoSplit[1],
+						Repo:             c.Repo(),
+						ProjectID:        cluster.ProjectID,
+						ClusterID:        cluster.ID,
+						UserID:           user.ID,
+						IntegrationID:    gitAction.GitlabIntegrationID,
+						PorterConf:       c.Config(),
+						ReleaseName:      helmRelease.Name,
+						ReleaseNamespace: helmRelease.Namespace,
+					}
+
+					err = giRunner.Cleanup()
+
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
+				} else {
+					gaRunner, err := getGARunner(
+						c.Config(),
+						user.ID,
+						cluster.ProjectID,
+						cluster.ID,
+						rel.GitActionConfig,
+						helmRelease.Name,
+						helmRelease.Namespace,
+						rel,
+						helmRelease,
+					)
+
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
 
-				err = gaRunner.Cleanup()
+					err = gaRunner.Cleanup()
 
-				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-					return
+					if err != nil {
+						c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+						return
+					}
 				}
 			}
 		}

+ 12 - 8
api/types/git_action_config.go

@@ -11,9 +11,12 @@ type GitActionConfig struct {
 	// The complete image repository uri to pull from
 	ImageRepoURI string `json:"image_repo_uri"`
 
-	// The git integration id
+	// The github integration ID
 	GitRepoID uint `json:"git_repo_id"`
 
+	// The gitlab integration ID
+	GitlabIntegrationID uint `json:"gitlab_integration_id"`
+
 	// The path to the dockerfile in the git repo
 	DockerfilePath string `json:"dockerfile_path"`
 
@@ -22,13 +25,14 @@ type GitActionConfig struct {
 }
 
 type CreateGitActionConfigRequest struct {
-	GitRepo        string `json:"git_repo" form:"required"`
-	GitBranch      string `json:"git_branch"`
-	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
-	DockerfilePath string `json:"dockerfile_path"`
-	FolderPath     string `json:"folder_path"`
-	GitRepoID      uint   `json:"git_repo_id" form:"required"`
-	RegistryID     uint   `json:"registry_id"`
+	GitRepo             string `json:"git_repo" form:"required"`
+	GitBranch           string `json:"git_branch"`
+	ImageRepoURI        string `json:"image_repo_uri" form:"required"`
+	DockerfilePath      string `json:"dockerfile_path"`
+	FolderPath          string `json:"folder_path"`
+	GitRepoID           uint   `json:"git_repo_id"`
+	GitlabIntegrationID uint   `json:"gitlab_integration_id"`
+	RegistryID          uint   `json:"registry_id"`
 
 	ShouldCreateWorkflow bool `json:"should_create_workflow"`
 }

+ 5 - 5
api/types/release.go

@@ -45,11 +45,11 @@ type CreateReleaseBaseRequest struct {
 type CreateReleaseRequest struct {
 	*CreateReleaseBaseRequest
 
-	ImageURL           string                        `json:"image_url" form:"required"`
-	GithubActionConfig *CreateGitActionConfigRequest `json:"github_action_config,omitempty"`
-	BuildConfig        *CreateBuildConfigRequest     `json:"build_config,omitempty"`
-	Tags               []string                      `json:"tags,omitempty"`
-	SyncedEnvGroups    []string                      `json:"synced_env_groups,omitempty"`
+	ImageURL        string                        `json:"image_url" form:"required"`
+	GitActionConfig *CreateGitActionConfigRequest `json:"git_action_config,omitempty"`
+	BuildConfig     *CreateBuildConfigRequest     `json:"build_config,omitempty"`
+	Tags            []string                      `json:"tags,omitempty"`
+	SyncedEnvGroups []string                      `json:"synced_env_groups,omitempty"`
 }
 
 type CreateAddonRequest struct {

+ 1 - 1
cli/cmd/deploy/create.go

@@ -135,7 +135,7 @@ func (c *CreateAgent) CreateFromGithub(
 				Name:            opts.ReleaseName,
 			},
 			ImageURL: imageURL,
-			GithubActionConfig: &types.CreateGitActionConfigRequest{
+			GitActionConfig: &types.CreateGitActionConfigRequest{
 				GitRepo:              ghOpts.Repo,
 				GitBranch:            ghOpts.Branch,
 				ImageRepoURI:         imageURL,

+ 1 - 1
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -312,7 +312,7 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
           template_name: props.currentTemplate.name.toLowerCase().trim(),
           template_version: props.currentTemplate?.currentVersion || "latest",
           name: release_name,
-          github_action_config: githubActionConfig,
+          git_action_config: githubActionConfig,
           build_config: buildConfig,
           synced_env_groups: synced.map((s: any) => s.name),
         },

+ 1 - 1
dashboard/src/shared/api.tsx

@@ -444,7 +444,7 @@ const deployTemplate = baseApi<
     image_url?: string;
     values?: any;
     name: string;
-    github_action_config?: FullActionConfigType;
+    git_action_config?: FullActionConfigType;
     build_config?: any;
     synced_env_groups?: string[];
   },

+ 303 - 0
internal/integrations/ci/gitlab/ci.go

@@ -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, "_", "-")))
+}

+ 10 - 6
internal/models/gitrepo.go

@@ -43,6 +43,9 @@ type GitActionConfig struct {
 	// The git repo ID (legacy field)
 	GitRepoID uint `json:"git_repo_id"`
 
+	// The gitlab integration ID
+	GitlabIntegrationID uint `json:"gitlab_integration_id"`
+
 	// The path to the dockerfile in the git repo
 	DockerfilePath string `json:"dockerfile_path"`
 
@@ -58,11 +61,12 @@ type GitActionConfig struct {
 // ToGitActionConfigType generates an external GitActionConfig to be shared over REST
 func (r *GitActionConfig) ToGitActionConfigType() *types.GitActionConfig {
 	return &types.GitActionConfig{
-		GitRepo:        r.GitRepo,
-		GitBranch:      r.GitBranch,
-		ImageRepoURI:   r.ImageRepoURI,
-		GitRepoID:      r.GitRepoID,
-		DockerfilePath: r.DockerfilePath,
-		FolderPath:     r.FolderPath,
+		GitRepo:             r.GitRepo,
+		GitBranch:           r.GitBranch,
+		ImageRepoURI:        r.ImageRepoURI,
+		GitRepoID:           r.GitRepoID,
+		GitlabIntegrationID: r.GitlabIntegrationID,
+		DockerfilePath:      r.DockerfilePath,
+		FolderPath:          r.FolderPath,
 	}
 }

+ 16 - 0
internal/oauth/config.go

@@ -152,6 +152,22 @@ func MakeUpdateGithubAppOauthIntegrationFunction(
 	}
 }
 
+// MakeUpdateGitlabAppOAuthIntegrationFunction creates a function to be passed to GetAccessToken that updates the GitlabAppOAuthIntegration
+// if it needs to be updated
+func MakeUpdateGitlabAppOAuthIntegrationFunction(
+	o *integrations.GitlabAppOAuthIntegration,
+	repo repository.Repository) func(accessToken []byte, refreshToken []byte, expiry time.Time) error {
+	return func(accessToken []byte, refreshToken []byte, expiry time.Time) error {
+		o.AccessToken = accessToken
+		o.RefreshToken = refreshToken
+		o.Expiry = expiry
+
+		_, err := repo.GitlabAppOAuthIntegration().UpdateGitlabAppOAuthIntegration(o)
+
+		return err
+	}
+}
+
 // GetAccessToken retrieves an access token for a given client. It updates the
 // access token in the DB if necessary
 func GetAccessToken(

+ 28 - 0
internal/repository/gorm/auth.go

@@ -1805,6 +1805,34 @@ func (repo *GitlabAppOAuthIntegrationRepository) ReadGitlabAppOAuthIntegration(
 	return gi, nil
 }
 
+func (repo *GitlabAppOAuthIntegrationRepository) UpdateGitlabAppOAuthIntegration(
+	gi *ints.GitlabAppOAuthIntegration,
+) (*ints.GitlabAppOAuthIntegration, error) {
+	err := repo.EncryptGitlabAppOAuthIntegrationData(gi, repo.key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// if storage backend is not nil, strip out credential data, which will be stored in credential
+	// storage backend after write to DB
+	// var credentialData = &credentials.GitlabCredential{}
+
+	// if repo.storageBackend != nil {
+	// 	credentialData.AppClientID = gi.AppClientID
+	// 	credentialData.AppClientSecret = gi.AppClientSecret
+
+	// 	gi.AppClientID = []byte{}
+	// 	gi.AppClientSecret = []byte{}
+	// }
+
+	if err := repo.db.Save(gi).Error; err != nil {
+		return nil, err
+	}
+
+	return gi, nil
+}
+
 // EncryptGitlabAppOAuthIntegrationData will encrypt the gitlab app oauth integration data before
 // writing to the DB
 func (repo *GitlabAppOAuthIntegrationRepository) EncryptGitlabAppOAuthIntegrationData(

+ 1 - 0
internal/repository/integrations.go

@@ -98,4 +98,5 @@ type GitlabIntegrationRepository interface {
 type GitlabAppOAuthIntegrationRepository interface {
 	CreateGitlabAppOAuthIntegration(gi *ints.GitlabAppOAuthIntegration) (*ints.GitlabAppOAuthIntegration, error)
 	ReadGitlabAppOAuthIntegration(userID, projectID, integrationID uint) (*ints.GitlabAppOAuthIntegration, error)
+	UpdateGitlabAppOAuthIntegration(gi *ints.GitlabAppOAuthIntegration) (*ints.GitlabAppOAuthIntegration, error)
 }