Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC-007] Implement GitHub app authentication for git repositories in IAC #780

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/spec/v1beta2/imageupdateautomations.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ patches:
azure.workload.identity/use: "true"
```

##### GitHub

If the provider is set to `github`, make sure the GitHub App is registered and
installed with the necessary permissions and the github app secret is configured
as described
[here](https://fluxcd.io/flux/components/source/gitrepositories/#github).

### Git specification

`.spec.git` is a required field to specify Git configurations related to source
Expand Down
20 changes: 19 additions & 1 deletion internal/source/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/fluxcd/pkg/auth/azure"
"github.com/fluxcd/pkg/auth/github"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
Expand Down Expand Up @@ -181,13 +182,30 @@ func getAuthOpts(ctx context.Context, c client.Client, repo *sourcev1.GitReposit
return nil, fmt.Errorf("failed to configure authentication options: %w", err)
}

if repo.GetProvider() == sourcev1.GitProviderAzure {
switch repo.GetProvider() {
case sourcev1.GitProviderAzure:
opts.ProviderOpts = &git.ProviderOptions{
Name: sourcev1.GitProviderAzure,
AzureOpts: []azure.OptFunc{
azure.WithAzureDevOpsScope(),
},
}
case sourcev1.GitProviderGitHub:
// if provider is github, but secret ref is not specified
if repo.Spec.SecretRef == nil {
return nil, fmt.Errorf("secretRef with github app data must be specified when provider is set to github: %w", ErrInvalidSourceConfiguration)
}
opts.ProviderOpts = &git.ProviderOptions{
Name: sourcev1.GitProviderGitHub,
GitHubOpts: []github.OptFunc{
github.WithAppData(data),
},
}
default:
// analyze secret, if it has github app data, perhaps provider should have been github.
if appID := data[github.AppIDKey]; len(appID) != 0 {
return nil, fmt.Errorf("secretRef '%s/%s' has github app data but provider is not set to github: %w", repo.GetNamespace(), repo.Spec.SecretRef.Name, ErrInvalidSourceConfiguration)
}
}

return opts, nil
Expand Down
96 changes: 88 additions & 8 deletions internal/source/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package source

import (
"context"
"errors"
"fmt"
"testing"
"time"
Expand All @@ -34,6 +35,7 @@ import (
imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
"github.com/fluxcd/image-automation-controller/internal/testutil"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/auth/github"
"github.com/fluxcd/pkg/git"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
Expand Down Expand Up @@ -141,49 +143,127 @@ func Test_getAuthOpts(t *testing.T) {
func Test_getAuthOpts_providerAuth(t *testing.T) {
tests := []struct {
name string
url string
secret *corev1.Secret
beforeFunc func(obj *sourcev1.GitRepository)
wantProviderOptsName string
wantErr error
}{
{
name: "azure provider",
url: "https://dev.azure.com/foo/bar/_git/baz",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderAzure
},
wantProviderOptsName: sourcev1.GitProviderAzure,
},
{
name: "github provider with no secret ref",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
},
wantProviderOptsName: sourcev1.GitProviderGitHub,
wantErr: errors.New("secretRef with github app data must be specified when provider is set to github: invalid source configuration"),
},
{
name: "github provider with secret ref that does not exist",
url: "https://github.com/org/repo.git",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: errors.New("failed to get auth secret '/githubAppSecret': secrets \"githubAppSecret\" not found"),
},
{
name: "github provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.AppIDKey: []byte("123"),
github.AppInstallationIDKey: []byte("456"),
github.AppPrivateKey: []byte("abc"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGitHub
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantProviderOptsName: sourcev1.GitProviderGitHub,
},
{
name: "generic provider with github app data in secret",
url: "https://example.com/org/repo",
secret: &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "githubAppSecret",
},
Data: map[string][]byte{
github.AppIDKey: []byte("123"),
},
},
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
obj.Spec.SecretRef = &meta.LocalObjectReference{
Name: "githubAppSecret",
}
},
wantErr: errors.New("secretRef '/githubAppSecret' has github app data but provider is not set to github: invalid source configuration"),
},
{
name: "generic provider",
url: "https://example.com/org/repo",
beforeFunc: func(obj *sourcev1.GitRepository) {
obj.Spec.Provider = sourcev1.GitProviderGeneric
},
},
{
name: "no provider",
url: "https://example.com/org/repo",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
clientBuilder := fakeclient.NewClientBuilder().
WithScheme(scheme.Scheme).
WithStatusSubresource(&sourcev1.GitRepository{})

if tt.secret != nil {
clientBuilder.WithObjects(tt.secret)
}
c := clientBuilder.Build()
obj := &sourcev1.GitRepository{
Spec: sourcev1.GitRepositorySpec{
URL: "https://dev.azure.com/foo/bar/_git/baz",
URL: tt.url,
},
}

if tt.beforeFunc != nil {
tt.beforeFunc(obj)
}
opts, err := getAuthOpts(context.TODO(), nil, obj)
opts, err := getAuthOpts(context.TODO(), c, obj)

g.Expect(err).ToNot(HaveOccurred())
g.Expect(opts).ToNot(BeNil())
if tt.wantProviderOptsName != "" {
g.Expect(opts.ProviderOpts).ToNot(BeNil())
g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName))
if tt.wantErr != nil {
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
} else {
g.Expect(opts.ProviderOpts).To(BeNil())
g.Expect(err).ToNot(HaveOccurred())
g.Expect(opts).ToNot(BeNil())
if tt.wantProviderOptsName != "" {
g.Expect(opts.ProviderOpts).ToNot(BeNil())
g.Expect(opts.ProviderOpts.Name).To(Equal(tt.wantProviderOptsName))
} else {
g.Expect(opts.ProviderOpts).To(BeNil())
}
}
})
}
Expand Down