fix(operator): reject a Git cert secret before installing CRDs and Flux

CRDs and Flux are installed before the platform source, so refusing a
cert secret on a Git source only when the source is written exits the
operator after a partial install. The check now runs right after flag
parsing, and the source install reuses it.

The guard runs on every start, so a Git source without a cert secret is
now covered over https and ssh: a guard that misfired there would
crash-loop the operator for every Git platform source. The update test
also renames the cert Secret, as a CA rotation does.

Assisted-by: LLM
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
This commit is contained in:
Kirill Ilin committed 2026-11-06 18:28:45 +05:00
1 parent 7f1227e3bd
commit dfd7cbed36
2 files changed
+94 -18

No files matched your search

+20 -2
View File
@@ -191,6 +191,11 @@ func main() {
os.Exit(1)
}
if err := validatePlatformSourceCertSecret(platformSourceURL, platformSourceCertSecret); err != nil {
setupLog.Error(err, "invalid platform source flags")
os.Exit(1)
}
config := ctrl.GetConfigOrDie()
// Create a direct client (without cache) for pre-start operations
@@ -439,8 +444,8 @@ func installPlatformSourceResource(ctx context.Context, k8sClient client.Client,
return fmt.Errorf("failed to generate OCIRepository: %w", err)
}
case "git":
if certSecretName != "" {
return fmt.Errorf("--platform-source-cert-secret is only supported for OCI sources; for a Git source put ca.crt into the --platform-source-secret Secret")
if err := validatePlatformSourceCertSecret(sourceURL, certSecretName); err != nil {
return err
}
obj, err = generateGitRepository(resourceName, repoURL, refMap, secretName)
if err != nil {
@@ -537,6 +542,19 @@ func parseSystemNamespaceMemory(rawLimit, rawRequest string) (limit, request res
return limit, request, nil
}
// validatePlatformSourceCertSecret rejects --platform-source-cert-secret for a
// Git source: GitRepository has no certSecretRef, and Flux reads a Git CA from
// the ca.crt key of the --platform-source-secret Secret instead.
func validatePlatformSourceCertSecret(sourceURL, certSecretName string) error {
if certSecretName == "" {
return nil
}
if sourceType, _, err := parsePlatformSourceURL(sourceURL); err == nil && sourceType == "git" {
return fmt.Errorf("--platform-source-cert-secret is only supported for OCI sources; for a Git source put ca.crt into the --platform-source-secret Secret")
}
return nil
}
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
// Supports formats:
// - oci://registry.example.com/repo
+74 -16
View File
@@ -612,26 +612,31 @@ func TestGenerateOCIRepository_WithCertSecret(t *testing.T) {
}
}
// The operator rewrites the whole platform OCIRepository on every start, so a
// certSecretRef patched onto the live object by hand is lost on the next
// restart and source-controller then fails TLS against a registry behind a
// private CA. The CA has to come from the operator's own arguments and survive
// that update path.
func TestInstallPlatformSourceResource_UpdateKeepsCertSecret(t *testing.T) {
func newSourceTestClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
scheme := newTestScheme()
if err := sourcev1.AddToScheme(scheme); err != nil {
t.Fatalf("add source scheme: %v", err)
}
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(objs...).Build()
}
// The operator rewrites the whole platform OCIRepository on every start, so a
// certSecretRef patched onto the live object by hand is lost on the next
// restart and source-controller then fails TLS against a registry behind a
// private CA. The CA has to come from the operator's own arguments and survive
// that update path, including a rename of the Secret when the CA is rotated.
func TestInstallPlatformSourceResource_UpdateKeepsCertSecret(t *testing.T) {
existing := &sourcev1.OCIRepository{
ObjectMeta: metav1.ObjectMeta{Name: "cozystack-platform", Namespace: "cozy-system"},
Spec: sourcev1.OCIRepositorySpec{URL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages"},
}
k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing).Build()
k8sClient := newSourceTestClient(t, existing)
for range 2 {
for _, certSecret := range []string{"registry-ca", "registry-ca-next"} {
if err := installPlatformSourceResource(context.Background(), k8sClient,
"oci://registry.internal:8083/cozystack/cozystack/cozystack-packages", "cozystack-platform",
"digest=sha256:abc123", "registry-creds", "registry-ca"); err != nil {
"digest=sha256:abc123", "registry-creds", certSecret); err != nil {
t.Fatalf("installPlatformSourceResource: %v", err)
}
}
@@ -640,8 +645,8 @@ func TestInstallPlatformSourceResource_UpdateKeepsCertSecret(t *testing.T) {
if err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: "cozy-system", Name: "cozystack-platform"}, got); err != nil {
t.Fatalf("get OCIRepository: %v", err)
}
if got.Spec.CertSecretRef == nil || got.Spec.CertSecretRef.Name != "registry-ca" {
t.Errorf("expected CertSecretRef registry-ca after update, got %+v", got.Spec.CertSecretRef)
if got.Spec.CertSecretRef == nil || got.Spec.CertSecretRef.Name != "registry-ca-next" {
t.Errorf("expected CertSecretRef registry-ca-next after update, got %+v", got.Spec.CertSecretRef)
}
if got.Spec.SecretRef == nil || got.Spec.SecretRef.Name != "registry-creds" {
t.Errorf("expected SecretRef registry-creds after update, got %+v", got.Spec.SecretRef)
@@ -655,11 +660,7 @@ func TestInstallPlatformSourceResource_UpdateKeepsCertSecret(t *testing.T) {
// of the secretRef Secret. Accepting the flag for a Git source would silently
// drop the CA, so it must fail before anything is written.
func TestInstallPlatformSourceResource_GitRejectsCertSecret(t *testing.T) {
scheme := newTestScheme()
if err := sourcev1.AddToScheme(scheme); err != nil {
t.Fatalf("add source scheme: %v", err)
}
k8sClient := fake.NewClientBuilder().WithScheme(scheme).Build()
k8sClient := newSourceTestClient(t)
err := installPlatformSourceResource(context.Background(), k8sClient,
"https://git.internal/cozystack/cozystack", "cozystack-platform", "branch=main", "git-creds", "git-ca")
@@ -679,6 +680,63 @@ func TestInstallPlatformSourceResource_GitRejectsCertSecret(t *testing.T) {
}
}
// The Git guard runs on every start; one that misfires without a cert secret
// would crash-loop the operator for every Git platform source.
func TestInstallPlatformSourceResource_GitWithoutCertSecret(t *testing.T) {
for _, sourceURL := range []string{
"https://git.internal/cozystack/cozystack",
"ssh://git@git.internal/cozystack/cozystack",
} {
t.Run(sourceURL, func(t *testing.T) {
k8sClient := newSourceTestClient(t)
if err := installPlatformSourceResource(context.Background(), k8sClient,
sourceURL, "cozystack-platform", "branch=main", "git-creds", ""); err != nil {
t.Fatalf("installPlatformSourceResource: %v", err)
}
got := &sourcev1.GitRepository{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Namespace: "cozy-system", Name: "cozystack-platform"}, got); err != nil {
t.Fatalf("get GitRepository: %v", err)
}
if got.Spec.URL != sourceURL {
t.Errorf("expected URL %q, got %q", sourceURL, got.Spec.URL)
}
if got.Spec.SecretRef == nil || got.Spec.SecretRef.Name != "git-creds" {
t.Errorf("expected SecretRef git-creds, got %+v", got.Spec.SecretRef)
}
})
}
}
// Checked right after flag parsing: CRDs and Flux are installed before the
// platform source, so a combination rejected only there would exit after a
// partial install.
func TestValidatePlatformSourceCertSecret(t *testing.T) {
tests := []struct {
name string
sourceURL string
certSecret string
wantErr bool
}{
{name: "oci with cert secret", sourceURL: "oci://registry.internal:8083/cozystack-packages", certSecret: "registry-ca"},
{name: "oci without cert secret", sourceURL: "oci://registry.internal:8083/cozystack-packages"},
{name: "https git without cert secret", sourceURL: "https://git.internal/cozystack/cozystack"},
{name: "ssh git without cert secret", sourceURL: "ssh://git@git.internal/cozystack/cozystack"},
{name: "no source url", certSecret: "registry-ca"},
{name: "https git with cert secret", sourceURL: "https://git.internal/cozystack/cozystack", certSecret: "git-ca", wantErr: true},
{name: "ssh git with cert secret", sourceURL: "ssh://git@git.internal/cozystack/cozystack", certSecret: "git-ca", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePlatformSourceCertSecret(tt.sourceURL, tt.certSecret)
if (err != nil) != tt.wantErr {
t.Errorf("validatePlatformSourceCertSecret(%q, %q) error = %v, wantErr %v", tt.sourceURL, tt.certSecret, err, tt.wantErr)
}
})
}
}
func TestGenerateGitRepository(t *testing.T) {
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap, "")