Provisioning: Add pure git repository type (#106815)
* Add repository type git to spec * Register git type * Update test checks
This commit is contained in:
@@ -381,3 +381,108 @@ func TestExportWorker_ProcessBranchNotAllowedForClonableRepositories(t *testing.
|
||||
err := r.Process(context.Background(), mockRepo, job, mockProgress)
|
||||
require.EqualError(t, err, "branch is not supported for clonable repositories")
|
||||
}
|
||||
|
||||
func TestExportWorker_ProcessGitRepository(t *testing.T) {
|
||||
job := v0alpha1.Job{
|
||||
Spec: v0alpha1.JobSpec{
|
||||
Action: v0alpha1.JobActionPush,
|
||||
Push: &v0alpha1.ExportJobOptions{},
|
||||
},
|
||||
}
|
||||
|
||||
mockRepo := repository.NewMockRepository(t)
|
||||
mockRepo.On("Config").Return(&v0alpha1.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Spec: v0alpha1.RepositorySpec{
|
||||
Type: v0alpha1.GitRepositoryType,
|
||||
Git: &v0alpha1.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow},
|
||||
},
|
||||
})
|
||||
|
||||
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
||||
// Verify progress messages are set
|
||||
mockProgress.On("SetMessage", mock.Anything, "clone target").Return()
|
||||
mockProgress.On("SetMessage", mock.Anything, "push changes").Return()
|
||||
|
||||
mockClients := resources.NewMockClientFactory(t)
|
||||
mockResourceClients := resources.NewMockResourceClients(t)
|
||||
mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil)
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything).Return(mockRepoResourcesClient, nil)
|
||||
|
||||
mockExportFn := NewMockExportFn(t)
|
||||
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
mockCloneFn := NewMockWrapWithCloneFn(t)
|
||||
// Verify clone and push options
|
||||
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.MatchedBy(func(opts repository.CloneOptions) bool {
|
||||
return opts.Timeout == 10*time.Minute && !opts.PushOnWrites && opts.BeforeFn != nil
|
||||
}), mock.MatchedBy(func(opts repository.PushOptions) bool {
|
||||
return opts.Timeout == 10*time.Minute && opts.Progress != nil && opts.BeforeFn != nil
|
||||
}), mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
|
||||
// Execute both BeforeFn functions to verify progress messages
|
||||
assert.NoError(t, cloneOpts.BeforeFn())
|
||||
assert.NoError(t, pushOpts.BeforeFn())
|
||||
|
||||
return fn(repo, true)
|
||||
})
|
||||
|
||||
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
|
||||
err := r.Process(context.Background(), mockRepo, job, mockProgress)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestExportWorker_ProcessGitRepositoryExportFnError(t *testing.T) {
|
||||
job := v0alpha1.Job{
|
||||
Spec: v0alpha1.JobSpec{
|
||||
Action: v0alpha1.JobActionPush,
|
||||
Push: &v0alpha1.ExportJobOptions{},
|
||||
},
|
||||
}
|
||||
|
||||
mockRepo := repository.NewMockRepository(t)
|
||||
mockRepo.On("Config").Return(&v0alpha1.Repository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-repo",
|
||||
Namespace: "test-namespace",
|
||||
},
|
||||
Spec: v0alpha1.RepositorySpec{
|
||||
Type: v0alpha1.GitRepositoryType,
|
||||
Git: &v0alpha1.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
Workflows: []v0alpha1.Workflow{v0alpha1.WriteWorkflow},
|
||||
},
|
||||
})
|
||||
|
||||
mockProgress := jobs.NewMockJobProgressRecorder(t)
|
||||
mockClients := resources.NewMockClientFactory(t)
|
||||
mockResourceClients := resources.NewMockResourceClients(t)
|
||||
mockClients.On("Clients", mock.Anything, "test-namespace").Return(mockResourceClients, nil)
|
||||
|
||||
mockRepoResources := resources.NewMockRepositoryResourcesFactory(t)
|
||||
mockRepoResourcesClient := resources.NewMockRepositoryResources(t)
|
||||
mockRepoResources.On("Client", mock.Anything, mock.Anything).Return(mockRepoResourcesClient, nil)
|
||||
|
||||
mockExportFn := NewMockExportFn(t)
|
||||
mockExportFn.On("Execute", mock.Anything, "test-repo", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("export failed"))
|
||||
|
||||
mockCloneFn := NewMockWrapWithCloneFn(t)
|
||||
mockCloneFn.On("Execute", mock.Anything, mockRepo, mock.Anything, mock.Anything, mock.Anything).Return(func(ctx context.Context, repo repository.Repository, cloneOpts repository.CloneOptions, pushOpts repository.PushOptions, fn func(repository.Repository, bool) error) error {
|
||||
return fn(repo, true)
|
||||
})
|
||||
|
||||
r := NewExportWorker(mockClients, mockRepoResources, mockExportFn.Execute, mockCloneFn.Execute)
|
||||
err := r.Process(context.Background(), mockRepo, job, mockProgress)
|
||||
require.EqualError(t, err, "export failed")
|
||||
}
|
||||
|
||||
@@ -414,6 +414,28 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis
|
||||
r.Spec.GitHub.URL = strings.TrimSuffix(r.Spec.GitHub.URL, "/")
|
||||
}
|
||||
}
|
||||
if r.Spec.Type == provisioning.GitRepositoryType {
|
||||
if r.Spec.Git == nil {
|
||||
return fmt.Errorf("git configuration is required")
|
||||
}
|
||||
|
||||
if r.Spec.GitHub != nil {
|
||||
return fmt.Errorf("git and github cannot be used together")
|
||||
}
|
||||
|
||||
if r.Spec.Local != nil {
|
||||
return fmt.Errorf("git and local cannot be used together")
|
||||
}
|
||||
|
||||
// Trim trailing slash and ensure .git is present
|
||||
if len(r.Spec.Git.URL) > 5 {
|
||||
r.Spec.Git.URL = strings.TrimSuffix(r.Spec.Git.URL, "/")
|
||||
|
||||
if !strings.HasSuffix(r.Spec.Git.URL, ".git") {
|
||||
r.Spec.Git.URL = r.Spec.Git.URL + ".git"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Spec.Workflows == nil {
|
||||
r.Spec.Workflows = []provisioning.Workflow{}
|
||||
@@ -423,6 +445,10 @@ func (b *APIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admis
|
||||
return fmt.Errorf("failed to encrypt secrets: %w", err)
|
||||
}
|
||||
|
||||
if err := b.encryptGitToken(ctx, r); err != nil {
|
||||
return fmt.Errorf("failed to encrypt secrets: %w", err)
|
||||
}
|
||||
|
||||
// Mutate the repository with any extra mutators
|
||||
for _, extra := range b.extras {
|
||||
if err := extra.Mutate(ctx, r); err != nil {
|
||||
@@ -448,6 +474,21 @@ func (b *APIBuilder) encryptGithubToken(ctx context.Context, repo *provisioning.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move this to a more appropriate place
|
||||
func (b *APIBuilder) encryptGitToken(ctx context.Context, repo *provisioning.Repository) error {
|
||||
var err error
|
||||
if repo.Spec.Git != nil &&
|
||||
repo.Spec.Git.Token != "" {
|
||||
repo.Spec.Git.EncryptedToken, err = b.secrets.Encrypt(ctx, []byte(repo.Spec.Git.Token))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo.Spec.Git.Token = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: move logic to a more appropriate place. Probably controller/validation.go
|
||||
func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
obj := a.GetObject()
|
||||
@@ -1126,6 +1167,14 @@ func (b *APIBuilder) AsRepository(ctx context.Context, r *provisioning.Repositor
|
||||
switch r.Spec.Type {
|
||||
case provisioning.LocalRepositoryType:
|
||||
return repository.NewLocal(r, b.localFileResolver), nil
|
||||
case provisioning.GitRepositoryType:
|
||||
return nanogit.NewGitRepository(ctx, b.secrets, r, nanogit.RepositoryConfig{
|
||||
URL: r.Spec.Git.URL,
|
||||
Branch: r.Spec.Git.Branch,
|
||||
Path: r.Spec.Git.Path,
|
||||
Token: r.Spec.Git.Token,
|
||||
EncryptedToken: r.Spec.Git.EncryptedToken,
|
||||
})
|
||||
case provisioning.GitHubRepositoryType:
|
||||
cloneFn := func(ctx context.Context, opts repository.CloneOptions) (repository.ClonedRepository, error) {
|
||||
return gogit.Clone(ctx, b.clonedir, r, opts, b.secrets)
|
||||
|
||||
@@ -76,11 +76,16 @@ func ValidateRepository(repo Repository) field.ErrorList {
|
||||
cfg.Spec.GitHub, "Github config only valid when type is github"))
|
||||
}
|
||||
|
||||
if cfg.Spec.Type != provisioning.GitRepositoryType && cfg.Spec.Git != nil {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", "git"),
|
||||
cfg.Spec.Git, "Git config only valid when type is git"))
|
||||
}
|
||||
|
||||
for _, w := range cfg.Spec.Workflows {
|
||||
switch w {
|
||||
case provisioning.WriteWorkflow: // valid; no fall thru
|
||||
case provisioning.BranchWorkflow:
|
||||
if cfg.Spec.Type != provisioning.GitHubRepositoryType {
|
||||
if !cfg.Spec.Type.IsGit() {
|
||||
list = append(list, field.Invalid(field.NewPath("spec", "workflow"), w, "branch is only supported on git repositories"))
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -151,6 +151,25 @@ func TestValidateRepository(t *testing.T) {
|
||||
require.Contains(t, errors.ToAggregate().Error(), "spec.github: Invalid value")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mismatched git config",
|
||||
repository: func() *MockRepository {
|
||||
m := NewMockRepository(t)
|
||||
m.On("Config").Return(&provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Title: "Test Repo",
|
||||
Type: provisioning.LocalRepositoryType,
|
||||
Git: &provisioning.GitRepositoryConfig{},
|
||||
},
|
||||
})
|
||||
m.On("Validate").Return(field.ErrorList{})
|
||||
return m
|
||||
}(),
|
||||
expectedErrs: 1,
|
||||
validateError: func(t *testing.T, errors field.ErrorList) {
|
||||
require.Contains(t, errors.ToAggregate().Error(), "spec.git: Invalid value")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple validation errors",
|
||||
repository: func() *MockRepository {
|
||||
|
||||
@@ -17,7 +17,7 @@ func IsWriteAllowed(repo *provisioning.Repository, ref string) error {
|
||||
case provisioning.WriteWorkflow:
|
||||
supportsWrite = true
|
||||
case provisioning.BranchWorkflow:
|
||||
supportsBranch = repo.Spec.Type == provisioning.GitHubRepositoryType
|
||||
supportsBranch = repo.Spec.Type.IsGit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ func IsWriteAllowed(repo *provisioning.Repository, ref string) error {
|
||||
ref = ""
|
||||
}
|
||||
|
||||
// Ref may be the configured branch for git repositories
|
||||
if ref != "" && repo.Spec.Git != nil && repo.Spec.Git.Branch == ref {
|
||||
ref = ""
|
||||
}
|
||||
|
||||
switch {
|
||||
case ref == "" && !supportsWrite:
|
||||
return apierrors.NewBadRequest("this repository does not support the write workflow")
|
||||
|
||||
@@ -135,6 +135,191 @@ func TestIsWriteAllowed(t *testing.T) {
|
||||
expectedErr: "this repository does not support the branch workflow",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "write workflow allowed on git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "write allowed for configured branch of git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "develop",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "develop",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "write not allowed for configured branch of git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "main",
|
||||
wantErr: true,
|
||||
expectedErr: "this repository does not support the write workflow",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "write workflow not allowed for git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: true,
|
||||
expectedErr: "this repository does not support the write workflow",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "branch workflow allowed on git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "feature-branch",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "branch workflow not allowed on git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "feature-branch",
|
||||
wantErr: true,
|
||||
expectedErr: "this repository does not support the branch workflow",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "both workflows allowed on git repository - write workflow",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow, provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "both workflows allowed on git repository - branch workflow",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow, provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "feature-branch",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "read only git repository",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "main",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: true,
|
||||
expectedErr: "this repository is read only",
|
||||
statusCode: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "git repository with empty branch config - write workflow",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "git repository with empty branch config - branch workflow",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.BranchWorkflow},
|
||||
Git: &provisioning.GitRepositoryConfig{
|
||||
URL: "https://git.example.com/repo.git",
|
||||
Branch: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
ref: "custom-branch",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "git repository without git config",
|
||||
repository: &provisioning.Repository{
|
||||
Spec: provisioning.RepositorySpec{
|
||||
Type: provisioning.GitRepositoryType,
|
||||
Workflows: []provisioning.Workflow{provisioning.WriteWorkflow},
|
||||
Git: nil,
|
||||
},
|
||||
},
|
||||
ref: "",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user