Skip to content

Commit

Permalink
Merge pull request #161 from syntasso/gitauthor
Browse files Browse the repository at this point in the history
feat: make git author configurable
  • Loading branch information
ChunyiLyu authored Jun 10, 2024
2 parents 53767d4 + 788e412 commit d1ec9ef
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 37 deletions.
26 changes: 16 additions & 10 deletions api/v1alpha1/gitstatestore_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,31 @@ type GitStateStoreSpec struct {

StateStoreCoreFields `json:",inline"`

//+kubebuilder:validation:Optional
//+kubebuilder:default=main
// +kubebuilder:validation:Optional
// +kubebuilder:default=main
Branch string `json:"branch,omitempty"`

// AuthMethod used to access the StateStore
//+kubebuilder:validation:Enum=basicAuth;ssh
//+kubebuilder:default:=basicAuth
// +kubebuilder:validation:Enum=basicAuth;ssh
// +kubebuilder:default:=basicAuth
AuthMethod string `json:"authMethod,omitempty"`
// Git author name and email used to commit this git state store; name defaults to 'kratix'
// +kubebuilder:default:={name: "kratix"}
GitAuthor GitAuthor `json:"gitAuthor,omitempty"`
}

type GitAuthor struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
}

// GitStateStoreStatus defines the observed state of GitStateStore
type GitStateStoreStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of GitStateStore
// Important: Run "make" to regenerate code after modifying this file
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster,path=gitstatestores,categories=kratix
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,path=gitstatestores,categories=kratix

// GitStateStore is the Schema for the gitstatestores API
type GitStateStore struct {
Expand All @@ -65,7 +71,7 @@ func (g *GitStateStore) GetSecretRef() *corev1.SecretReference {
return g.Spec.SecretRef
}

//+kubebuilder:object:root=true
// +kubebuilder:object:root=true

// GitStateStoreList contains a list of GitStateStore
type GitStateStoreList struct {
Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/crd/bases/platform.kratix.io_gitstatestores.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ spec:
branch:
default: main
type: string
gitAuthor:
default:
name: kratix
description: Git author name and email used to commit this git state
store; name defaults to 'kratix'
properties:
email:
type: string
name:
type: string
type: object
path:
description: |-
Path within the StateStore to write documents. This path should be allocated
Expand Down
54 changes: 27 additions & 27 deletions lib/writers/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import (
)

type GitWriter struct {
GitServer gitServer
Author gitAuthor
Path string
Log logr.Logger
gitServer gitServer
author gitAuthor
path string
}

type gitServer struct {
Expand All @@ -39,19 +39,19 @@ func NewGitWriter(logger logr.Logger, stateStoreSpec v1alpha1.GitStateStoreSpec,
var authMethod transport.AuthMethod
switch stateStoreSpec.AuthMethod {
case v1alpha1.SSHAuthMethod:
sshKey, ok := creds["sshPrivateKey"]
sshPrivateKey, ok := creds["sshPrivateKey"]
if !ok {
return nil, fmt.Errorf("sshPrivateKey not found in secret %s/%s", destination.Namespace, stateStoreSpec.SecretRef.Name)
return nil, fmt.Errorf("sshKey not found in secret %s/%s", destination.Namespace, stateStoreSpec.SecretRef.Name)
}

knownHosts, ok := creds["knownHosts"]
if !ok {
return nil, fmt.Errorf("knownHosts not found in secret %s/%s", destination.Namespace, stateStoreSpec.SecretRef.Name)
}

sshPrivateKey, err := ssh.NewPublicKeys("git", sshKey, "")
sshKey, err := ssh.NewPublicKeys("git", sshPrivateKey, "")
if err != nil {
return nil, fmt.Errorf("error parsing sshPrivateKey: %w", err)
return nil, fmt.Errorf("error parsing sshKey: %w", err)
}

knownHostsFile, err := os.CreateTemp("", "knownHosts")
Expand All @@ -65,13 +65,13 @@ func NewGitWriter(logger logr.Logger, stateStoreSpec v1alpha1.GitStateStoreSpec,
return nil, fmt.Errorf("error parsing known hosts: %w", err)
}

sshPrivateKey.HostKeyCallback = knownHostsCallback
sshKey.HostKeyCallback = knownHostsCallback
err = os.Remove(knownHostsFile.Name())
if err != nil {
return nil, fmt.Errorf("error removing knownhosts file: %w", err)
}

authMethod = sshPrivateKey
authMethod = sshKey
case v1alpha1.BasicAuthMethod:
username, ok := creds["username"]
if !ok {
Expand All @@ -90,17 +90,17 @@ func NewGitWriter(logger logr.Logger, stateStoreSpec v1alpha1.GitStateStoreSpec,
}

return &GitWriter{
gitServer: gitServer{
GitServer: gitServer{
URL: stateStoreSpec.URL,
Branch: stateStoreSpec.Branch,
Auth: authMethod,
},
author: gitAuthor{
Name: "Kratix",
Email: "kratix@syntasso.io",
Author: gitAuthor{
Name: stateStoreSpec.GitAuthor.Name,
Email: stateStoreSpec.GitAuthor.Email,
},
Log: logger,
path: filepath.Join(stateStoreSpec.Path, destination.Spec.Path, destination.Namespace, destination.Name),
Path: filepath.Join(stateStoreSpec.Path, destination.Spec.Path, destination.Namespace, destination.Name),
}, nil
}

Expand All @@ -113,10 +113,10 @@ func (g *GitWriter) update(subDir, workPlacementName string, workloadsToCreate [
return "", nil
}

dirInGitRepo := filepath.Join(g.path, subDir)
dirInGitRepo := filepath.Join(g.Path, subDir)
logger := g.Log.WithValues(
"dir", dirInGitRepo,
"branch", g.gitServer.Branch,
"branch", g.GitServer.Branch,
)

localTmpDir, repo, worktree, err := g.setupLocalDirectoryWithRepo(logger)
Expand All @@ -141,12 +141,12 @@ func (g *GitWriter) update(subDir, workPlacementName string, workloadsToCreate [
absoluteFilePath := filepath.Join(localTmpDir, worktreeFilePath)

//We need to protect against paths containing `..`
//filepath.Join expands any '../' in the path to the actual, e.g. /tmp/foo/../ resolves to /tmp/
//To ensure they can't write to files on disk outside the tmp git repository we check the absolute path
//filepath.Join expands any '../' in the Path to the actual, e.g. /tmp/foo/../ resolves to /tmp/
//To ensure they can't write to files on disk outside the tmp git repository we check the absolute Path
//returned by `filepath.Join` is still contained with the git repository:
// Note: This means `../` can still be used, but only if the end result is still contained within the git repository
if !strings.HasPrefix(absoluteFilePath, localTmpDir) {
log.Error(nil, "path of file to write is not located within the git repostiory", "absolutePath", absoluteFilePath, "tmpDir", localTmpDir)
log.Error(nil, "Path of file to write is not located within the git repostiory", "absolutePath", absoluteFilePath, "tmpDir", localTmpDir)
return "", nil //We don't want to retry as this isn't a recoverable error. Log error and return nil.
}

Expand Down Expand Up @@ -206,8 +206,8 @@ func (g *GitWriter) deleteExistingFiles(removeDirectory bool, dir string, worklo

func (g *GitWriter) ReadFile(filePath string) ([]byte, error) {
logger := g.Log.WithValues(
"path", filePath,
"branch", g.gitServer.Branch,
"Path", filePath,
"branch", g.GitServer.Branch,
)

localTmpDir, _, worktree, err := g.setupLocalDirectoryWithRepo(logger)
Expand Down Expand Up @@ -247,7 +247,7 @@ func (g *GitWriter) setupLocalDirectoryWithRepo(logger logr.Logger) (string, *gi
func (g *GitWriter) push(repo *git.Repository, logger logr.Logger) error {
err := repo.Push(&git.PushOptions{
RemoteName: "origin",
Auth: g.gitServer.Auth,
Auth: g.GitServer.Auth,
InsecureSkipTLS: true,
})
if err != nil {
Expand All @@ -260,9 +260,9 @@ func (g *GitWriter) push(repo *git.Repository, logger logr.Logger) error {
func (g *GitWriter) cloneRepo(localRepoFilePath string, logger logr.Logger) (*git.Repository, error) {
logger.Info("cloning repo")
return git.PlainClone(localRepoFilePath, false, &git.CloneOptions{
Auth: g.gitServer.Auth,
URL: g.gitServer.URL,
ReferenceName: plumbing.NewBranchReferenceName(g.gitServer.Branch),
Auth: g.GitServer.Auth,
URL: g.GitServer.URL,
ReferenceName: plumbing.NewBranchReferenceName(g.GitServer.Branch),
SingleBranch: true,
Depth: 1,
NoCheckout: false,
Expand All @@ -284,8 +284,8 @@ func (g *GitWriter) commitAndPush(repo *git.Repository, worktree *git.Worktree,

commitHash, err := worktree.Commit(fmt.Sprintf("%s from: %s", action, workPlacementName), &git.CommitOptions{
Author: &object.Signature{
Name: g.author.Name,
Email: g.author.Email,
Name: g.Author.Name,
Email: g.Author.Email,
When: time.Now(),
},
})
Expand Down
107 changes: 107 additions & 0 deletions lib/writers/git_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package writers_test

import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/go-logr/logr"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/syntasso/kratix/api/v1alpha1"
"github.com/syntasso/kratix/lib/writers"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"log"
ctrl "sigs.k8s.io/controller-runtime"
)

var _ = Describe("NewGitWriter", func() {
var (
logger logr.Logger
dest v1alpha1.Destination
stateStoreSpec v1alpha1.GitStateStoreSpec
)

BeforeEach(func() {
logger = ctrl.Log.WithName("setup")
stateStoreSpec = v1alpha1.GitStateStoreSpec{
AuthMethod: "basicAuth",
URL: "https://github.com/syntasso/kratix",
Branch: "test",
GitAuthor: v1alpha1.GitAuthor{
Email: "test@example.com",
Name: "a-user",
},
}

dest = v1alpha1.Destination{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "test",
},
Spec: v1alpha1.DestinationSpec{},
}

})

It("returns a valid GitWriter", func() {
creds := map[string][]byte{
"username": []byte("user1"),
"password": []byte("pw1"),
}
stateStoreSpec.GitAuthor = v1alpha1.GitAuthor{
Email: "test@example.com",
Name: "a-user",
}
writer, err := writers.NewGitWriter(logger, stateStoreSpec, dest, creds)
Expect(err).NotTo(HaveOccurred())
Expect(writer).To(BeAssignableToTypeOf(&writers.GitWriter{}))
gitWriter, ok := writer.(*writers.GitWriter)
Expect(ok).To(BeTrue())
Expect(gitWriter.GitServer.URL).To(Equal("https://github.com/syntasso/kratix"))
Expect(gitWriter.GitServer.Auth).To(Equal(&http.BasicAuth{
Username: "user1",
Password: "pw1",
}))
Expect(gitWriter.GitServer.Branch).To(Equal("test"))
Expect(gitWriter.Author.Email).To(Equal("test@example.com"))
Expect(gitWriter.Author.Name).To(Equal("a-user"))
})

Context("authenticate with SSH", func() {
It("returns a valid GitWriter", func() {
stateStoreSpec.AuthMethod = "ssh"
key, err := rsa.GenerateKey(rand.Reader, 1024)
Expect(err).NotTo(HaveOccurred())
privateKeyPEM := pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
var b bytes.Buffer
if err := pem.Encode(&b, &privateKeyPEM); err != nil {
log.Fatalf("Failed to write private key to buffer: %v", err)
}

creds := map[string][]byte{
"sshPrivateKey": b.Bytes(),
"knownHosts": []byte("github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"),
}

writer, err := writers.NewGitWriter(logger, stateStoreSpec, dest, creds)
Expect(err).NotTo(HaveOccurred())
Expect(writer).To(BeAssignableToTypeOf(&writers.GitWriter{}))
gitWriter, ok := writer.(*writers.GitWriter)
Expect(ok).To(BeTrue())
Expect(gitWriter.GitServer.URL).To(Equal("https://github.com/syntasso/kratix"))
publicKey, ok := gitWriter.GitServer.Auth.(*ssh.PublicKeys)
Expect(ok).To(BeTrue())
Expect(publicKey).NotTo(BeNil())
Expect(gitWriter.GitServer.Branch).To(Equal("test"))
Expect(gitWriter.Author.Email).To(Equal("test@example.com"))
Expect(gitWriter.Author.Name).To(Equal("a-user"))
})
})
})

0 comments on commit d1ec9ef

Please sign in to comment.