controllers/codebase/service/chain/put_project.go (390 lines of code) (raw):
package chain
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
"github.com/epam/edp-codebase-operator/v2/pkg/gerrit"
"github.com/epam/edp-codebase-operator/v2/pkg/git"
"github.com/epam/edp-codebase-operator/v2/pkg/gitprovider"
"github.com/epam/edp-codebase-operator/v2/pkg/util"
)
type PutProject struct {
client client.Client
git git.Git
gerrit gerrit.Client
gitProjectProvider func(gitServer *codebaseApi.GitServer, token string) (gitprovider.GitProjectProvider, error)
}
var (
skipPutProjectStatuses = []string{util.ProjectPushedStatus, util.ProjectTemplatesPushedStatus}
putProjectStrategies = []codebaseApi.Strategy{codebaseApi.Clone, codebaseApi.Create}
)
func NewPutProject(
c client.Client,
g git.Git,
gerritProvider gerrit.Client,
gitProjectProvider func(gitServer *codebaseApi.GitServer, token string) (gitprovider.GitProjectProvider, error),
) *PutProject {
return &PutProject{client: c, git: g, gerrit: gerritProvider, gitProjectProvider: gitProjectProvider}
}
func (h *PutProject) ServeRequest(ctx context.Context, codebase *codebaseApi.Codebase) error {
log := ctrl.LoggerFrom(ctx).WithValues("projectID", codebase.Spec.GetProjectID())
if h.skip(ctx, codebase) {
return nil
}
log.Info("Start putting project", "spec", codebase.Spec)
err := setIntermediateSuccessFields(ctx, h.client, codebase, codebaseApi.GerritRepositoryProvisioning)
if err != nil {
return fmt.Errorf("failed to update Codebase %v status: %w", codebase.Name, err)
}
wd := util.GetWorkDir(codebase.Name, codebase.Namespace)
if err = util.CreateDirectory(wd); err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return fmt.Errorf("failed to create dir %q: %w", wd, err)
}
gitServer := &codebaseApi.GitServer{}
if err = h.client.Get(
ctx,
client.ObjectKey{Name: codebase.Spec.GitServer, Namespace: codebase.Namespace},
gitServer,
); err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return fmt.Errorf("failed to get GitServer %s: %w", codebase.Spec.GitServer, err)
}
err = h.initialProjectProvisioning(ctx, codebase, wd)
if err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return fmt.Errorf("failed to perform initial provisioning of codebase %v: %w", codebase.Name, err)
}
if err = h.checkoutBranch(ctrl.LoggerInto(ctx, log), codebase, wd); err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return err
}
err = h.createProject(ctrl.LoggerInto(ctx, log), codebase, gitServer, wd)
if err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return fmt.Errorf("failed to create project: %w", err)
}
codebase.Status.Git = util.ProjectPushedStatus
if err = h.client.Status().Update(ctx, codebase); err != nil {
setFailedFields(codebase, codebaseApi.GerritRepositoryProvisioning, err.Error())
return fmt.Errorf("failed to set git status %s for codebase %s: %w", util.ProjectPushedStatus, codebase.Name, err)
}
log.Info("Finish putting project")
return nil
}
func (*PutProject) skip(ctx context.Context, codebase *codebaseApi.Codebase) bool {
log := ctrl.LoggerFrom(ctx)
if !slices.Contains(putProjectStrategies, codebase.Spec.Strategy) {
log.Info("Skip putting project to repository for non-clone or non-create strategy")
return true
}
if slices.Contains(skipPutProjectStatuses, codebase.Status.Git) {
log.Info("Skipping putting project, it has been already pushed")
return true
}
return false
}
func (h *PutProject) createProject(
ctx context.Context,
codebase *codebaseApi.Codebase,
gitServer *codebaseApi.GitServer,
workDir string,
) error {
gitServerSecret := &corev1.Secret{}
if err := h.client.Get(ctx, client.ObjectKey{Name: gitServer.Spec.NameSshKeySecret, Namespace: codebase.Namespace}, gitServerSecret); err != nil {
return fmt.Errorf("failed to get git server secret: %w", err)
}
privateSSHKey := string(gitServerSecret.Data[util.PrivateSShKeyName])
gitProviderToken := string(gitServerSecret.Data[util.GitServerSecretTokenField])
if gitServer.Spec.GitProvider == codebaseApi.GitProviderGerrit {
err := h.createGerritProject(ctx, gitServer, privateSSHKey, codebase.Spec.GetProjectID())
if err != nil {
return fmt.Errorf("failed to create project in Gerrit for codebase %v: %w", codebase.Name, err)
}
} else {
if err := h.createGitThirdPartyProject(ctx, gitServer, gitProviderToken, codebase.Spec.GetProjectID()); err != nil {
return err
}
}
err := h.pushProject(ctx, gitServer, privateSSHKey, codebase.Spec.GetProjectID(), workDir)
if err != nil {
return err
}
err = h.setDefaultBranch(ctx, gitServer, codebase, gitProviderToken, privateSSHKey)
if err != nil {
return err
}
return nil
}
func (h *PutProject) replaceDefaultBranch(ctx context.Context, directory, defaultBranchName, newBranchName string) error {
log := ctrl.LoggerFrom(ctx).
WithValues("defaultBranch", defaultBranchName, "newBranch", newBranchName)
log.Info("Replacing default branch with new one")
log.Info("Removing default branch")
if err := h.git.RemoveBranch(directory, defaultBranchName); err != nil {
return fmt.Errorf("failed to remove master branch: %w", err)
}
log.Info("Creating new branch")
if err := h.git.CreateChildBranch(directory, newBranchName, defaultBranchName); err != nil {
return fmt.Errorf("failed to create child branch: %w", err)
}
log.Info("Project has been successfully created")
return nil
}
func (h *PutProject) pushProject(ctx context.Context, gitServer *codebaseApi.GitServer, privateSSHKey, projectName, directory string) error {
log := ctrl.LoggerFrom(ctx).WithValues("gitProvider", gitServer.Spec.GitProvider)
log.Info("Start pushing project")
log.Info("Start adding remote link to Gerrit")
if err := h.git.AddRemoteLink(
directory,
util.GetSSHUrl(gitServer, projectName),
); err != nil {
return fmt.Errorf("failed to add remote link to Gerrit: %w", err)
}
log.Info("Start pushing changes into git")
if err := h.git.PushChanges(privateSSHKey, gitServer.Spec.GitUser, directory, gitServer.Spec.SshPort, "--all"); err != nil {
return fmt.Errorf("failed to push changes: %w", err)
}
log.Info("Start pushing tags into git")
if err := h.git.PushChanges(privateSSHKey, gitServer.Spec.GitUser, directory, gitServer.Spec.SshPort, "--tags"); err != nil {
return fmt.Errorf("failed to push changes into git: %w", err)
}
log.Info("Project has been pushed successfully")
return nil
}
func (h *PutProject) createGerritProject(ctx context.Context, gitServer *codebaseApi.GitServer, privateSSHKey, projectName string) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Start creating project in Gerrit")
projectExist, err := h.gerrit.CheckProjectExist(gitServer.Spec.SshPort, privateSSHKey, gitServer.Spec.GitHost, gitServer.Spec.GitUser, projectName, log)
if err != nil {
return fmt.Errorf("failed to check if project exist in Gerrit: %w", err)
}
if projectExist {
log.Info("Skip creating project in Gerrit, project already exist")
return nil
}
err = h.gerrit.CreateProject(gitServer.Spec.SshPort, privateSSHKey, gitServer.Spec.GitHost, gitServer.Spec.GitUser, projectName, log)
if err != nil {
return fmt.Errorf("failed to create gerrit project: %w", err)
}
log.Info("Project created in Gerrit")
return nil
}
func (h *PutProject) checkoutBranch(ctx context.Context, codebase *codebaseApi.Codebase, workDir string) error {
log := ctrl.LoggerFrom(ctx).WithValues(
"defaultBranch",
codebase.Spec.DefaultBranch,
"branchToCopy",
codebase.Spec.BranchToCopyInDefaultBranch,
)
repoUrl, err := util.GetRepoUrl(codebase)
if err != nil {
return fmt.Errorf("failed to build repo url: %w", err)
}
if codebase.Spec.BranchToCopyInDefaultBranch != "" && codebase.Spec.DefaultBranch != codebase.Spec.BranchToCopyInDefaultBranch {
log.Info("Start checkout branch to copy")
err = CheckoutBranch(repoUrl, workDir, codebase.Spec.BranchToCopyInDefaultBranch, h.git, codebase, h.client)
if err != nil {
return fmt.Errorf("failed to checkout default branch %s: %w", codebase.Spec.DefaultBranch, err)
}
log.Info("Start replace default branch")
err = h.replaceDefaultBranch(ctx, workDir, codebase.Spec.DefaultBranch, codebase.Spec.BranchToCopyInDefaultBranch)
if err != nil {
return fmt.Errorf("failed to replace master: %w", err)
}
return nil
}
log.Info("Start checkout branch")
err = CheckoutBranch(repoUrl, workDir, codebase.Spec.DefaultBranch, h.git, codebase, h.client)
if err != nil {
return fmt.Errorf("failed to checkout default branch %s: %w", codebase.Spec.DefaultBranch, err)
}
log.Info("Checkout branch finished")
return nil
}
func (h *PutProject) createGitThirdPartyProject(ctx context.Context, gitServer *codebaseApi.GitServer, gitProviderToken, projectName string) error {
log := ctrl.LoggerFrom(ctx).WithValues("gitProvider", gitServer.Spec.GitProvider)
log.Info("Start creating project in git provider")
gitProvider, err := h.gitProjectProvider(gitServer, gitProviderToken)
if err != nil {
return fmt.Errorf("failed to create git provider: %w", err)
}
projectExists, err := gitProvider.ProjectExists(
ctx,
gitprovider.GetGitProviderAPIURL(gitServer),
gitProviderToken,
projectName,
)
if err != nil {
return fmt.Errorf("failed to check if project exists: %w", err)
}
if projectExists {
log.Info("Skip creating project in git provider, project already exist")
return nil
}
if err = gitProvider.CreateProject(
ctx,
gitprovider.GetGitProviderAPIURL(gitServer),
gitProviderToken,
projectName,
); err != nil {
return fmt.Errorf("failed to create project: %w", err)
}
log.Info("Project created in git provider")
return nil
}
func (h *PutProject) setDefaultBranch(
ctx context.Context,
gitServer *codebaseApi.GitServer,
codebase *codebaseApi.Codebase,
gitProviderToken,
privateSSHKey string,
) error {
log := ctrl.LoggerFrom(ctx).
WithValues("gitProvider", gitServer.Spec.GitProvider)
log.Info("Start setting default branch", "defaultBranch", codebase.Spec.DefaultBranch)
if gitServer.Spec.GitProvider == codebaseApi.GitProviderGerrit {
log.Info("Set HEAD to default branch in Gerrit")
err := h.gerrit.SetHeadToBranch(
gitServer.Spec.SshPort,
privateSSHKey,
gitServer.Spec.GitHost,
gitServer.Spec.GitUser,
codebase.Spec.GetProjectID(),
codebase.Spec.DefaultBranch,
log,
)
if err != nil {
return fmt.Errorf(
"set remote HEAD for codebase %s to default branch %s has been failed: %w",
codebase.Spec.GetProjectID(),
codebase.Spec.DefaultBranch,
err,
)
}
log.Info("Set HEAD to default branch in Gerrit has been finished")
return nil
}
log.Info("Set default branch in git provider")
gitProvider, err := h.gitProjectProvider(gitServer, gitProviderToken)
if err != nil {
return fmt.Errorf("failed to create git provider: %w", err)
}
if err = gitProvider.SetDefaultBranch(
ctx,
gitprovider.GetGitProviderAPIURL(gitServer),
gitProviderToken,
codebase.Spec.GetProjectID(),
codebase.Spec.DefaultBranch,
); err != nil {
if errors.Is(err, gitprovider.ErrApiNotSupported) {
// We can skip this error, because it is not supported by Git provider.
// And this is not critical for the whole process.
log.Info("Setting default branch is not supported by Git provider. Set it manually if needed")
return nil
}
return fmt.Errorf("failed to set default branch: %w", err)
}
log.Info("Default branch has been set")
return nil
}
func (h *PutProject) tryToCloneRepo(ctx context.Context, repoUrl string, repositoryUsername, repositoryPassword *string, workDir string) error {
log := ctrl.LoggerFrom(ctx).WithValues("dest", workDir, "repoUrl", repoUrl)
log.Info("Start cloning repository")
if util.DoesDirectoryExist(workDir + "/.git") {
log.Info("Repository already exists")
return nil
}
if err := h.git.CloneRepository(repoUrl, repositoryUsername, repositoryPassword, workDir); err != nil {
return fmt.Errorf("failed to clone repository: %w", err)
}
log.Info("Repository has been cloned")
return nil
}
func (h *PutProject) squashCommits(ctx context.Context, workDir string, strategy codebaseApi.Strategy) error {
log := ctrl.LoggerFrom(ctx).WithValues("dest", workDir, "strategy", strategy)
if strategy != codebaseApi.Create {
return nil
}
log.Info("Start squashing commits")
err := os.RemoveAll(workDir + "/.git")
if err != nil {
return fmt.Errorf("failed to remove .git folder: %w", err)
}
if err := h.git.Init(workDir); err != nil {
return fmt.Errorf("failed to create git repository: %w", err)
}
if err := h.git.CommitChanges(workDir, "Initial commit"); err != nil {
return fmt.Errorf("failed to commit all default content: %w", err)
}
log.Info("Commits have been squashed")
return nil
}
func (h *PutProject) initialProjectProvisioning(ctx context.Context, codebase *codebaseApi.Codebase, wd string) error {
if codebase.Spec.EmptyProject {
return h.emptyProjectProvisioning(ctx, wd)
}
return h.notEmptyProjectProvisioning(ctx, codebase, wd)
}
func (h *PutProject) emptyProjectProvisioning(ctx context.Context, wd string) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Initialing empty git repository")
if err := h.git.Init(wd); err != nil {
return fmt.Errorf("failed to create empty git repository: %w", err)
}
log.Info("Making initial commit")
if err := h.git.CommitChanges(wd, "Initial commit", git.CommitAllowEmpty()); err != nil {
return fmt.Errorf("failed to create Initial commit: %w", err)
}
return nil
}
func (h *PutProject) notEmptyProjectProvisioning(ctx context.Context, codebase *codebaseApi.Codebase, wd string) error {
log := ctrl.LoggerFrom(ctx)
log.Info("Start initial provisioning for non-empty project")
repoUrl, err := util.GetRepoUrl(codebase)
if err != nil {
return fmt.Errorf("failed to build repo url: %w", err)
}
repu, repp, err := GetRepositoryCredentialsIfExists(codebase, h.client)
// we are ok if no credentials is found, assuming this is a public repo
if err != nil && !k8sErrors.IsNotFound(err) {
return fmt.Errorf("failed to get repository credentials: %w", err)
}
if !h.git.CheckPermissions(ctx, repoUrl, repu, repp) {
return fmt.Errorf("failed to get access to the repository %v for user %v", repoUrl, *repu)
}
if err = h.tryToCloneRepo(ctx, repoUrl, repu, repp, wd); err != nil {
return fmt.Errorf("failed to clone template project: %w", err)
}
if err = h.squashCommits(ctx, wd, codebase.Spec.Strategy); err != nil {
return fmt.Errorf("failed to squash commits in a template repo: %w", err)
}
return nil
}
func setFailedFields(c *codebaseApi.Codebase, a codebaseApi.ActionType, message string) {
// Set WebHookRef from WebHookID for backward compatibility.
webHookRef := c.Status.WebHookRef
if webHookRef == "" && c.Status.WebHookID != 0 {
webHookRef = strconv.Itoa(c.Status.WebHookID)
}
c.Status = codebaseApi.CodebaseStatus{
Status: util.StatusFailed,
Available: false,
LastTimeUpdated: metaV1.Now(),
Username: "system",
Action: a,
Result: codebaseApi.Error,
DetailedMessage: message,
Value: "failed",
FailureCount: c.Status.FailureCount,
Git: c.Status.Git,
WebHookID: c.Status.WebHookID,
WebHookRef: webHookRef,
GitWebUrl: c.Status.GitWebUrl,
}
}