Skip to content
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/argoproj-labs/gitops-promoter v0.35.0
github.com/argoproj/argo-cd/gitops-engine v0.7.1-0.20250908182407-97ad5b59a627
github.com/argoproj/argo-cd/v3 v3.5.1
github.com/cert-manager/cert-manager v1.20.3
github.com/go-logr/logr v1.4.4
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518
Expand All @@ -22,6 +23,7 @@ require (
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.74.0
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.55.0
golang.org/x/mod v0.40.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools v2.2.0+incompatible
Expand Down Expand Up @@ -59,7 +61,6 @@ require (
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cert-manager/cert-manager v1.20.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.3 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
Expand Down Expand Up @@ -178,7 +179,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
Expand Down
8 changes: 8 additions & 0 deletions test/openshift/e2e/ginkgo/fixture/argocd/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func HavePhase(expected string) matcher.GomegaMatcher {
"Redis:", argocd.Status.Redis,
"Repo:", argocd.Status.Repo,
"Server: ", argocd.Status.Server,
"CommitServer:", argocd.Status.CommitServer,
"ApplicationController:", argocd.Status.ApplicationController,
"ApplicationSetController:", argocd.Status.ApplicationSetController,
"NotificationsController:", argocd.Status.NotificationsController,
Expand Down Expand Up @@ -119,6 +120,13 @@ func HaveServerStatus(status string) matcher.GomegaMatcher {
})
}

func HaveCommitServerStatus(status string) matcher.GomegaMatcher {
return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool {
GinkgoWriter.Println("HaveCommitServerStatus:", "expected:", status, "/ actual:", argocd.Status.CommitServer)
return argocd.Status.CommitServer == status
})
}

func HaveApplicationControllerStatus(status string) matcher.GomegaMatcher {
return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool {
GinkgoWriter.Println("HaveApplicationControllerStatus:", "expected:", status, "/ actual:", argocd.Status.ApplicationController)
Expand Down
2 changes: 1 addition & 1 deletion test/openshift/e2e/ginkgo/fixture/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ func OutputDebug(namespaceParams ...any) {

for _, namespace := range namespaces {

kubectlOutput, err := osFixture.ExecCommandWithOutputParam(false, true, "kubectl", "get", "all", "-n", namespace)
kubectlOutput, err := osFixture.ExecCommandWithOutputParam(false, true, "kubectl", "get", "all,serviceaccount", "-n", namespace)
if err != nil {
GinkgoWriter.Println("unable to list", namespace, err, kubectlOutput)
continue
Expand Down
79 changes: 79 additions & 0 deletions test/openshift/e2e/ginkgo/fixture/gitserver/certs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package gitserver

import (
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"fmt"
"strings"

certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
. "github.com/onsi/gomega"
"golang.org/x/crypto/ssh"

"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
)

type sshKeyPair struct {
privateKeyPEM []byte
publicKey string
}

func generateSSHKeyPair() sshKeyPair {
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
Expect(err).NotTo(HaveOccurred())

sshPublicKey, err := ssh.NewPublicKey(publicKey)
Expect(err).NotTo(HaveOccurred())

privateKeyBlock, err := ssh.MarshalPrivateKey(privateKey, "")
Expect(err).NotTo(HaveOccurred())

return sshKeyPair{
privateKeyPEM: pem.EncodeToMemory(privateKeyBlock),
publicKey: string(ssh.MarshalAuthorizedKey(sshPublicKey)),
}
}

func formatSSHKnownHosts(host string, port int32, publicKey ssh.PublicKey) string {
keyLine := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
parts := strings.SplitN(keyLine, " ", 3)
Expect(len(parts)).To(BeNumerically(">=", 2))

hostPort := host
if port != 22 {
hostPort = fmt.Sprintf("[%s]:%d", host, port)
}
return fmt.Sprintf("%s %s %s\n", hostPort, parts[0], parts[1])
}

func generateTLSSecretData(domain string, podName string, namespace string) map[string][]byte {
key, err := argoutil.NewPrivateKey()
Expect(err).NotTo(HaveOccurred())

caKey, err := argoutil.NewPrivateKey()
Expect(err).NotTo(HaveOccurred())
caCert, err := argoutil.NewSelfSignedCACertificate(domain, caKey)
Expect(err).NotTo(HaveOccurred())

certSpec := &certmanagerv1.CertificateSpec{
CommonName: domain,
Subject: &certmanagerv1.X509Subject{
Organizations: []string{domain},
},
}
dnsNames := []string{
podName,
fmt.Sprintf("%s.%s", podName, namespace),
fmt.Sprintf("%s.%s.svc", podName, namespace),
fmt.Sprintf("%s.%s.svc.cluster.local", podName, namespace),
}
cert, err := argoutil.NewSignedCertificate(certSpec, dnsNames, key, caCert, caKey)
Expect(err).NotTo(HaveOccurred())

return map[string][]byte{
"tls.crt": argoutil.EncodeCertificatePEM(cert),
"tls.key": argoutil.EncodePrivateKeyPEM(key),
"ca.crt": argoutil.EncodeCertificatePEM(caCert),
}
}
152 changes: 152 additions & 0 deletions test/openshift/e2e/ginkgo/fixture/gitserver/gitea.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package gitserver

import (
"encoding/base64"
"fmt"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"golang.org/x/crypto/ssh"

corev1 "k8s.io/api/core/v1"

argocdutil "github.com/argoproj-labs/argocd-operator/controllers/argocd"
osFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/os"
)

const (
giteaImage = "ghcr.io/go-gitea/gitea:1.26.4-rootless"
giteaCustomPath = "/data/gitea"
)

func giteaEnvVars(domain, internalToken string) []corev1.EnvVar {
return []corev1.EnvVar{
{Name: "USER_UID", Value: "1000"},
{Name: "USER_GID", Value: "1000"},
{Name: "GITEA_CUSTOM", Value: giteaCustomPath},
{Name: "GITEA_APP_NAME", Value: "Gitea E2E Git Server"},
{Name: "GITEA__run_mode", Value: "prod"},
{Name: "GITEA__database__DB_TYPE", Value: "sqlite3"},
{Name: "GITEA__database__PATH", Value: "/data/gitea/gitea.db"},
{Name: "GITEA__repository__DEFAULT_BRANCH", Value: "main"},
{Name: "GITEA__server__DOMAIN", Value: domain},
{Name: "GITEA__server__SSH_DOMAIN", Value: domain},
{Name: "GITEA__server__SSH_USER", Value: giteaSSHLogin},
{Name: "GITEA__server__HTTP_PORT", Value: fmt.Sprintf("%d", httpPort)},
{Name: "GITEA__server__ROOT_URL", Value: fmt.Sprintf("https://%s:%d/", domain, httpPort)},
{Name: "GITEA__server__PROTOCOL", Value: "https"},
{Name: "GITEA__server__CERT_FILE", Value: "/etc/gitea/certs/tls.crt"},
{Name: "GITEA__server__KEY_FILE", Value: "/etc/gitea/certs/tls.key"},
{Name: "GITEA__server__LOCAL_ROOT_URL", Value: fmt.Sprintf("https://127.0.0.1:%d/", httpPort)},
{Name: "GITEA__server__START_SSH_SERVER", Value: "true"},
{Name: "GITEA__server__SSH_PORT", Value: fmt.Sprintf("%d", sshServicePort)},
{Name: "GITEA__server__SSH_LISTEN_PORT", Value: fmt.Sprintf("%d", sshPort)},
{Name: "GITEA__server__DISABLE_SSH", Value: "false"},
{Name: "GITEA__server__LFS_START_SERVER", Value: "false"},
{Name: "GITEA__security__INSTALL_LOCK", Value: "true"},
{Name: "GITEA__security__INTERNAL_TOKEN", Value: internalToken},
{Name: "GITEA__security__SECRET_KEY", Value: argocdutil.GenerateRandomString(24)},
{Name: "GITEA__oauth2__JWT_SECRET", Value: argocdutil.GenerateRandomString(24)},
{Name: "GITEA__lfs__LFS_JWT_SECRET", Value: argocdutil.GenerateRandomString(24)},
{Name: "GITEA__service__DISABLE_REGISTRATION", Value: "true"},
{Name: "GITEA__service__REQUIRE_SIGNIN_VIEW", Value: "false"},
{Name: "GITEA__mailer__ENABLED", Value: "false"},
{Name: "GITEA__openid__ENABLE_OPENID_SIGNIN", Value: "false"},
{Name: "GITEA__openid__ENABLE_OPENID_SIGNUP", Value: "false"},
}
}

func configureGiteaAdmin(server *Server) {
By("configuring Gitea admin user and SSH key")

Eventually(func() error {
_, err := execInGiteaPod(server.namespace, "gitea", "admin", "user", "list")
return err
}, "30s", "5s").Should(Succeed())

Eventually(func() error {
out, err := execInGiteaPod(server.namespace,
"gitea", "admin", "user", "create",
"--username", gitUsername,
"--password", server.httpPassword,
"--email", "gituser@test.local",
"--admin",
"--must-change-password=false",
)
if err != nil && (strings.Contains(out, "already exists") || strings.Contains(out, "user already")) {
return nil
}
if err != nil {
GinkgoWriter.Printf("gitea admin user create failed: %v: %s\n", err, out)
}
return err
}, "30s", "5s").Should(Succeed())

Eventually(func() error {
out, err := giteaAPIPost(server.namespace, server.httpPassword,
"/api/v1/user/keys",
fmt.Sprintf(`{"title":"e2e-git-ssh-key","key":"%s"}`, strings.TrimSpace(server.sshPublicKey)),
)
if err != nil && strings.Contains(out, "already") {
return nil
}
if err != nil {
GinkgoWriter.Printf("gitea add SSH key failed: %v: %s\n", err, out)
}
return err
}, "30s", "5s").Should(Succeed())
}

// fetchSSHKnownHosts reads Gitea's generated SSH host public key from the pod and
// formats it for argocd-ssh-known-hosts-cm.
func fetchSSHKnownHosts(server *Server) string {
const hostPublicKeyPath = "/var/lib/gitea/ssh/gitea.rsa.pub"

var knownHosts string
Eventually(func(g Gomega) {
out, err := execInGiteaPod(server.namespace, "cat", hostPublicKeyPath)
g.Expect(err).NotTo(HaveOccurred(), out)

pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(out)))
g.Expect(err).NotTo(HaveOccurred(), "parse Gitea SSH host key from %q: %q", hostPublicKeyPath, out)

knownHosts = formatSSHKnownHosts(server.clusterDomain, sshServicePort, pubKey)
g.Expect(knownHosts).NotTo(BeEmpty())
}, "2m", "5s").Should(Succeed())

return knownHosts
}

func execInGiteaPod(namespace string, args ...string) (string, error) {
execArgs := []string{"kubectl", "exec", "-n", namespace, "pod/" + serverName, "-c", serverName, "--"}
execArgs = append(execArgs, args...)
return osFixture.ExecCommandWithOutputParam(false, false, execArgs...)
}

func giteaAPIPost(namespace, password, path, body string) (string, error) {
auth := base64.StdEncoding.EncodeToString([]byte(gitUsername + ":" + password))
return execInGiteaPod(namespace, giteaWgetArgs("Basic "+auth, "POST", path, body)...)
}

func giteaAPIGet(namespace, password, path string) (string, error) {
auth := base64.StdEncoding.EncodeToString([]byte(gitUsername + ":" + password))
return execInGiteaPod(namespace, giteaWgetArgs("Basic "+auth, "GET", path, "")...)
}

func giteaWgetArgs(auth, method, path, body string) []string {
args := []string{
"wget",
"-q", "--no-check-certificate",
"--header=Authorization: " + auth,
"-O", "-",
}
if method == "POST" {
args = append(args,
"--header=Content-Type: application/json",
"--post-data="+body,
)
}
args = append(args, fmt.Sprintf("https://127.0.0.1:%d%s", httpPort, path))
return args
}
Loading
Loading