Skip to content
Open
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/modules/ROOT/pages/how-tos/gitlab-connection.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ image::gitlab_settings.png[]

Before any other things can be created we need to specify the Git repository API endpoint first:

[NOTE]
====
`sshEndpoint` is optional. If omitted, the operator uses the same host as `endpoint` for SSH.
Provide a full SSH URL (for example `ssh://gitlab-ssh.example.com`) or just a host name.
====

[source,shell]
....
kubectl -n lieutenant create secret generic lieutenant-secret \
--from-literal endpoint=http://10.144.1.197:8080 \
--from-literal sshEndpoint=ssh://gitlab-ssh.example.com \
--from-literal token=<token>
....
24 changes: 12 additions & 12 deletions git/gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ func init() {
manager.Register(&Gitlab{})
}

var (
ListItemsPerPage = 100
)
var ListItemsPerPage = 100

// Gitlab holds the necessary information to communincate with a Gitlab server.
// Each Gitlab instance will handle exactly one project.
Expand All @@ -43,7 +41,6 @@ type Gitlab struct {

// Create will create a new Gitlab project
func (g *Gitlab) Create() error {

nsID, err := g.getNamespaceID()
if err != nil {
return err
Expand Down Expand Up @@ -136,7 +133,6 @@ func (g *Gitlab) updateDisplayName() (bool, error) {

func (g *Gitlab) removeDeployKeys(deleteKeys map[string]synv1alpha1.DeployKey) error {
existingKeys, _, err := g.client.DeployKeys.ListProjectDeployKeys(g.project.ID, &gitlab.ListProjectDeployKeysOptions{})

if err != nil {
return err
}
Expand Down Expand Up @@ -227,11 +223,21 @@ func (g *Gitlab) Connect() error {

// FullURL returns the complete url of this git repository
func (g *Gitlab) FullURL() *url.URL {

sshURL := *g.ops.URL

sshURL.Scheme = "ssh"
sshURL.User = url.User("git")
if g.ops.SSHHost != "" {
sshURL.Host = g.ops.SSHHost
// If the original URL had no host (for example "git.example.com/foo/bar"),
// the host ends up in Path. Strip it when overriding the host.
if g.ops.URL.Host == "" {
trimmed := strings.TrimPrefix(sshURL.Path, "/")
if parts := strings.SplitN(trimmed, "/", 2); len(parts) == 2 {
sshURL.Path = "/" + parts[1]
}
}
}
sshURL.Path = sshURL.Path + ".git"

return &sshURL
Expand Down Expand Up @@ -260,7 +266,6 @@ func (g *Gitlab) New(options manager.RepoOptions) (manager.Repo, error) {
}

func (g *Gitlab) getNamespaceID() (*int, error) {

fetchedNamespace, _, err := g.client.Namespaces.GetNamespace(g.ops.Path)
if err != nil {
return nil, err
Expand Down Expand Up @@ -338,7 +343,6 @@ func (g *Gitlab) getDeployKeys() (map[string]synv1alpha1.DeployKey, error) {

// CommitTemplateFiles uploads all defined template files onto the repository.
func (g *Gitlab) CommitTemplateFiles() error {

if len(g.ops.TemplateFiles) == 0 {
return nil
}
Expand Down Expand Up @@ -386,7 +390,6 @@ func (g *Gitlab) CommitTemplateFiles() error {
// files that should be created. If there are existing files they will be
// dropped.
func (g *Gitlab) compareFiles() ([]manager.CommitFile, error) {

files := make([]manager.CommitFile, 0)
resp := &gitlab.Response{NextPage: 1}
var trees []*gitlab.TreeNode
Expand Down Expand Up @@ -438,14 +441,12 @@ func (g *Gitlab) compareFiles() ([]manager.CommitFile, error) {
Content: content,
})
}

}

return files, nil
}

func (g *Gitlab) getCommitOptions() *gitlab.CreateCommitOptions {

co := &gitlab.CreateCommitOptions{
AuthorEmail: ptr.To("lieutenant-operator@syn.local"),
AuthorName: ptr.To("Lieutenant Operator"),
Expand Down Expand Up @@ -530,7 +531,6 @@ func (g *Gitlab) EnsureProjectAccessToken(ctx context.Context, name string, opts
Scopes: ptr.To([]string{"write_repository"}),
AccessLevel: ptr.To(gitlab.MaintainerPermissions),
}, gitlab.WithContext(ctx))

if err != nil {
return manager.ProjectAccessToken{}, fmt.Errorf("error response from gitlab when creating ProjectAccessToken: %w", err)
}
Expand Down
16 changes: 9 additions & 7 deletions git/gitlab/gitlab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ func TestGitlab_Read(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

defer tt.httpServer.Close()

serverURL, _ := url.Parse(tt.httpServer.URL)
Expand Down Expand Up @@ -176,7 +175,6 @@ func TestGitlab_Create(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

defer tt.httpServer.Close()

serverURL, _ := url.Parse(tt.httpServer.URL)
Expand Down Expand Up @@ -234,7 +232,6 @@ func TestGitlab_Delete(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

defer tt.httpServer.Close()

serverURL, _ := url.Parse(tt.httpServer.URL)
Expand Down Expand Up @@ -270,7 +267,6 @@ func testGetUpdateServer(t *testing.T, fail bool) *httptest.Server {
mux := http.NewServeMux()

mux.HandleFunc("/api/v4/projects/3/deploy_keys", func(res http.ResponseWriter, req *http.Request) {

respH := http.StatusOK
if fail {
respH = http.StatusInternalServerError
Expand Down Expand Up @@ -329,7 +325,6 @@ func testGetUpdateServer(t *testing.T, fail bool) *httptest.Server {
mux.HandleFunc("/", testutils.LogNotFoundHandler(t))

return httptest.NewServer(mux)

}

func TestGitlab_Update(t *testing.T) {
Expand Down Expand Up @@ -377,7 +372,6 @@ func TestGitlab_Update(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

defer tt.httpServer.Close()

serverURL, _ := url.Parse(tt.httpServer.URL)
Expand Down Expand Up @@ -601,7 +595,6 @@ func TestGitlab_CommitTemplateFiles(t *testing.T) {
ListItemsPerPage = 1 // simulate pagination
for name, tt := range tests {
t.Run(name, func(t *testing.T) {

tt.fields.ops.URL, _ = url.Parse(tt.httpServer.URL)

g := &Gitlab{
Expand Down Expand Up @@ -635,6 +628,15 @@ func TestGitlab_FullURL(t *testing.T) {
assert.Equal(t, expectedFullURL, g.FullURL().String())
assert.Equal(t, expectedFullURL, g.FullURL().String())
assert.Equal(t, expectedFullURL, g.FullURL().String())

sshHostExpected := "ssh://git@ssh.example.com/foo/bar.git"
g = &Gitlab{
ops: manager.RepoOptions{
URL: serverURL,
SSHHost: "ssh.example.com",
},
}
assert.Equal(t, sshHostExpected, g.FullURL().String())
}

type mockClock struct {
Expand Down
50 changes: 43 additions & 7 deletions git/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/url"
"strings"
"time"

synv1alpha1 "github.com/projectsyn/lieutenant-operator/api/v1alpha1"
Expand All @@ -23,15 +24,15 @@ const (
SecretHostKeysName = "hostKeys"
// SecretEndpointName is the name of the secret entry containing the api endpoint
SecretEndpointName = "endpoint"
// SecretSSHEndpointName is the name of the secret entry containing the ssh endpoint (optional)
SecretSSHEndpointName = "sshEndpoint"
// DeletionMagicString defines when a file should be deleted from the repository
//TODO it will be replaced with something better in the future TODO
// TODO it will be replaced with something better in the future TODO
DeletionMagicString = "{delete}"
)

var (
// implementations holds each a copy of the registered Git implementation
implementations []Implementation
)
// implementations holds each a copy of the registered Git implementation
var implementations []Implementation

// Register adds a type to the list of supported Git implementations.
func Register(i Implementation) {
Expand All @@ -40,7 +41,6 @@ func Register(i Implementation) {

// NewRepo returns a Repo object that can handle the specific URL
func NewRepo(opts RepoOptions) (Repo, error) {

for _, imp := range implementations {
if exists, err := imp.IsType(opts.URL); exists {
newImp, err := imp.New(opts)
Expand All @@ -66,6 +66,7 @@ type RepoOptions struct {
DeployKeys map[string]synv1alpha1.DeployKey
Logger logr.Logger
URL *url.URL
SSHHost string
Path string
RepoName string
DisplayName string
Expand Down Expand Up @@ -212,11 +213,19 @@ func GetGitClient(ctx context.Context, instance *synv1alpha1.GitRepoTemplate, na
}

repoURL, err := url.Parse(string(secret.Data[SecretEndpointName]) + "/" + instance.Path + "/" + instance.RepoName)

if err != nil {
return nil, "", err
}

sshHost := ""
if raw, ok := secret.Data[SecretSSHEndpointName]; ok {
parsed, err := parseSSHEndpoint(string(raw))
if err != nil {
return nil, "", fmt.Errorf("invalid ssh endpoint in secret %s: %w", secret.GetName(), err)
}
sshHost = parsed
}

repoOptions := RepoOptions{
Credentials: Credentials{
Token: string(secret.Data[SecretTokenName]),
Expand All @@ -227,6 +236,7 @@ func GetGitClient(ctx context.Context, instance *synv1alpha1.GitRepoTemplate, na
RepoName: instance.RepoName,
DisplayName: instance.DisplayName,
URL: repoURL,
SSHHost: sshHost,
TemplateFiles: instance.TemplateFiles,
DeletionPolicy: instance.DeletionPolicy,
}
Expand All @@ -239,5 +249,31 @@ func GetGitClient(ctx context.Context, instance *synv1alpha1.GitRepoTemplate, na
err = repo.Connect()

return repo, hostKeysString, err
}

func parseSSHEndpoint(raw string) (string, error) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return "", nil
}

parsed, err := url.Parse(trimmed)
if err != nil {
return "", err
}

if parsed.Host != "" {
return parsed.Host, nil
}

host := strings.TrimSpace(parsed.Path)
if host == "" {
return "", fmt.Errorf("ssh endpoint has no host")
}

if at := strings.LastIndex(host, "@"); at >= 0 {
host = host[at+1:]
}

return host, nil
}