From 2b3efc309239f4f135e4d0d0005a0acb61f9272c Mon Sep 17 00:00:00 2001 From: Greg Haynes Date: Thu, 18 Dec 2025 20:45:49 +0000 Subject: [PATCH 1/3] Add certificate export command for Docker registry trust Implements 'idpbuilder get certificate' command to export the self-signed TLS certificate from the cluster. This solves the Docker trust issue with the Gitea container registry without requiring a Docker daemon restart. Features: - Default: Prints certificate to stdout for flexibility - --docker flag: Exports to Docker's per-registry cert directory (~/.docker/certs.d//ca.crt) - --output flag: Exports to a custom file path - Provides platform-specific instructions for system-wide trust - Works without Docker restart, avoiding cluster shutdown Includes comprehensive test coverage for certificate retrieval, file operations, registry host determination, and command flags. Signed-off-by: Greg Haynes --- pkg/cmd/get/certificate.go | 179 ++++++++++++++++++++++++++ pkg/cmd/get/certificate_test.go | 219 ++++++++++++++++++++++++++++++++ pkg/cmd/get/root.go | 1 + 3 files changed, 399 insertions(+) create mode 100644 pkg/cmd/get/certificate.go create mode 100644 pkg/cmd/get/certificate_test.go diff --git a/pkg/cmd/get/certificate.go b/pkg/cmd/get/certificate.go new file mode 100644 index 000000000..32e44ea49 --- /dev/null +++ b/pkg/cmd/get/certificate.go @@ -0,0 +1,179 @@ +package get + +import ( +"context" +"fmt" +"os" +"path/filepath" +"runtime" + +"github.com/cnoe-io/idpbuilder/globals" +"github.com/cnoe-io/idpbuilder/pkg/util" +"github.com/spf13/cobra" +corev1 "k8s.io/api/core/v1" +"sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( +certOutputPath string +setupDocker bool +) + +var CertificateCmd = &cobra.Command{ +Use: "certificate", +Short: "Export the TLS certificate from the cluster", +Long: `Export the self-signed TLS certificate from the cluster. + +By default, the certificate is printed to stdout. Use the --docker flag to automatically +configure Docker's per-registry certificate directory, which allows Docker to trust the +Gitea container registry without requiring a Docker daemon restart. + +Examples: + # Print certificate to stdout + idpbuilder get certificate + + # Export to Docker's registry certificate directory (no Docker restart needed) + idpbuilder get certificate --docker + + # Export to a custom file + idpbuilder get certificate --output ~/my-cert.crt`, +RunE: getCertificateE, +SilenceUsage: true, +} + +func init() { +CertificateCmd.Flags().StringVarP(&certOutputPath, "output", "o", "", "Custom output path for the certificate file") +CertificateCmd.Flags().BoolVar(&setupDocker, "docker", false, "Setup Docker registry certificate directory") +} + +func getCertificateE(cmd *cobra.Command, args []string) error { +ctx, ctxCancel := context.WithCancel(cmd.Context()) +defer ctxCancel() + +kubeConfig, err := util.GetKubeConfig() +if err != nil { +return fmt.Errorf("getting kube config: %w", err) +} + +kubeClient, err := util.GetKubeClient(kubeConfig) +if err != nil { +return fmt.Errorf("getting kube client: %w", err) +} + +// Get the certificate from the cluster +cert, err := getCertificateFromCluster(ctx, kubeClient) +if err != nil { +return err +} + +// Get the build configuration to determine registry host +config, err := util.GetConfig(ctx) +if err != nil { +return fmt.Errorf("getting idp config: %w", err) +} + +var registryHost string +if config.UsePathRouting { +registryHost = fmt.Sprintf("%s:%s", config.Host, config.Port) +} else { +registryHost = fmt.Sprintf("gitea.%s:%s", config.Host, config.Port) +} + +// Determine output behavior +if certOutputPath != "" { +// Custom output path specified +if err := os.WriteFile(certOutputPath, cert, 0644); err != nil { +return fmt.Errorf("writing certificate to %s: %w", certOutputPath, err) +} +fmt.Printf("Certificate exported to: %s\n", certOutputPath) +return nil +} else if setupDocker { +// Docker's per-registry certificate directory +homeDir, err := os.UserHomeDir() +if err != nil { +return fmt.Errorf("getting user home directory: %w", err) +} + +dockerCertsDir := filepath.Join(homeDir, ".docker", "certs.d", registryHost) +if err := os.MkdirAll(dockerCertsDir, 0755); err != nil { +return fmt.Errorf("creating Docker certificate directory: %w", err) +} + +outputPath := filepath.Join(dockerCertsDir, "ca.crt") +if err := os.WriteFile(outputPath, cert, 0644); err != nil { +return fmt.Errorf("writing certificate to %s: %w", outputPath, err) +} + +fmt.Printf("Certificate exported successfully to: %s\n", outputPath) +fmt.Printf("Registry host: %s\n\n", registryHost) +printPostInstallInstructions(registryHost) +return nil +} + +// Default: print to stdout +fmt.Print(string(cert)) +return nil +} + +func getCertificateFromCluster(ctx context.Context, kubeClient client.Client) ([]byte, error) { +secret := &corev1.Secret{} +secretKey := client.ObjectKey{ +Name: globals.SelfSignedCertSecretName, +Namespace: globals.NginxNamespace, +} + +if err := kubeClient.Get(ctx, secretKey, secret); err != nil { +return nil, fmt.Errorf("getting certificate secret from cluster: %w. Make sure the cluster is running.", err) +} + +cert, ok := secret.Data[corev1.TLSCertKey] +if !ok { +return nil, fmt.Errorf("certificate not found in secret %s/%s", globals.NginxNamespace, globals.SelfSignedCertSecretName) +} + +return cert, nil +} + +func printPostInstallInstructions(registryHost string) { +fmt.Println("Next steps:") +fmt.Println(" 1. The certificate has been configured for Docker's per-registry trust") +fmt.Println(" 2. No Docker restart is required - the certificate should work immediately") +fmt.Println() +fmt.Println("To verify Docker can access the registry:") +fmt.Printf(" docker pull %s/test-image\n", registryHost) +fmt.Println() + +// Platform-specific instructions for system-wide trust (optional) +switch runtime.GOOS { +case "darwin": +fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") +fmt.Println(" 1. Find the certificate file:") +homeDir, _ := os.UserHomeDir() +fmt.Printf(" %s\n", filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt")) +fmt.Println(" 2. Double-click the certificate file to open Keychain Access") +fmt.Println(" 3. Select 'System' keychain and click 'Add'") +fmt.Println(" 4. Double-click the imported certificate") +fmt.Println(" 5. Expand 'Trust' section and set 'When using this certificate' to 'Always Trust'") +fmt.Println() +fmt.Println(" Or use the command line:") +certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") +fmt.Printf(" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain %s\n", certPath) + +case "linux": +fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") +homeDir, _ := os.UserHomeDir() +certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") +fmt.Printf(" sudo cp %s /usr/local/share/ca-certificates/idpbuilder-ca.crt\n", certPath) +fmt.Println(" sudo update-ca-certificates") + +case "windows": +fmt.Println("Optional: To trust this certificate system-wide:") +fmt.Println(" 1. Open the certificate file in Explorer") +fmt.Println(" 2. Click 'Install Certificate'") +fmt.Println(" 3. Select 'Local Machine' and click Next") +fmt.Println(" 4. Select 'Place all certificates in the following store'") +fmt.Println(" 5. Browse to 'Trusted Root Certification Authorities'") +fmt.Println(" 6. Click OK and Finish") +} +fmt.Println() +} diff --git a/pkg/cmd/get/certificate_test.go b/pkg/cmd/get/certificate_test.go new file mode 100644 index 000000000..c925d3088 --- /dev/null +++ b/pkg/cmd/get/certificate_test.go @@ -0,0 +1,219 @@ +package get + +import ( +"context" +"os" +"path/filepath" +"testing" + +"github.com/cnoe-io/idpbuilder/api/v1alpha1" +"github.com/cnoe-io/idpbuilder/globals" +"github.com/stretchr/testify/assert" +"github.com/stretchr/testify/mock" +corev1 "k8s.io/api/core/v1" +"k8s.io/apimachinery/pkg/api/errors" +"k8s.io/apimachinery/pkg/runtime/schema" +"sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestGetCertificateFromCluster(t *testing.T) { +ctx := context.Background() + +t.Run("successful certificate retrieval", func(t *testing.T) { +fClient := new(fakeKubeClient) +expectedCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") + +secretKey := client.ObjectKey{ +Name: globals.SelfSignedCertSecretName, +Namespace: globals.NginxNamespace, +} + +fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { +arg := args.Get(2).(*corev1.Secret) +arg.Data = map[string][]byte{ +corev1.TLSCertKey: expectedCert, +} +}).Return(nil) + +cert, err := getCertificateFromCluster(ctx, fClient) +assert.NoError(t, err) +assert.Equal(t, expectedCert, cert) +fClient.AssertExpectations(t) +}) + +t.Run("secret not found", func(t *testing.T) { +fClient := new(fakeKubeClient) + +secretKey := client.ObjectKey{ +Name: globals.SelfSignedCertSecretName, +Namespace: globals.NginxNamespace, +} + +notFoundErr := errors.NewNotFound(schema.GroupResource{Resource: "secrets"}, globals.SelfSignedCertSecretName) +fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Return(notFoundErr) + +cert, err := getCertificateFromCluster(ctx, fClient) +assert.Error(t, err) +assert.Nil(t, cert) +assert.Contains(t, err.Error(), "getting certificate secret from cluster") +fClient.AssertExpectations(t) +}) + +t.Run("certificate data missing from secret", func(t *testing.T) { +fClient := new(fakeKubeClient) + +secretKey := client.ObjectKey{ +Name: globals.SelfSignedCertSecretName, +Namespace: globals.NginxNamespace, +} + +fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { +arg := args.Get(2).(*corev1.Secret) +arg.Data = map[string][]byte{ +// Missing TLSCertKey +"other-key": []byte("value"), +} +}).Return(nil) + +cert, err := getCertificateFromCluster(ctx, fClient) +assert.Error(t, err) +assert.Nil(t, cert) +assert.Contains(t, err.Error(), "certificate not found in secret") +fClient.AssertExpectations(t) +}) +} + +func TestRegistryHostDetermination(t *testing.T) { +testCases := []struct { +name string +config v1alpha1.BuildCustomizationSpec +expectedHost string +}{ +{ +name: "subdomain routing", +config: v1alpha1.BuildCustomizationSpec{ +Host: "cnoe.localtest.me", +Port: "8443", +UsePathRouting: false, +}, +expectedHost: "gitea.cnoe.localtest.me:8443", +}, +{ +name: "path routing", +config: v1alpha1.BuildCustomizationSpec{ +Host: "cnoe.localtest.me", +Port: "8443", +UsePathRouting: true, +}, +expectedHost: "cnoe.localtest.me:8443", +}, +{ +name: "custom host with path routing", +config: v1alpha1.BuildCustomizationSpec{ +Host: "example.com", +Port: "443", +UsePathRouting: true, +}, +expectedHost: "example.com:443", +}, +{ +name: "custom host with subdomain routing", +config: v1alpha1.BuildCustomizationSpec{ +Host: "example.com", +Port: "443", +UsePathRouting: false, +}, +expectedHost: "gitea.example.com:443", +}, +} + +for _, tc := range testCases { +t.Run(tc.name, func(t *testing.T) { +var registryHost string +if tc.config.UsePathRouting { +registryHost = tc.config.Host + ":" + tc.config.Port +} else { +registryHost = "gitea." + tc.config.Host + ":" + tc.config.Port +} +assert.Equal(t, tc.expectedHost, registryHost) +}) +} +} + +func TestCertificateFileOperations(t *testing.T) { +t.Run("write certificate to custom path", func(t *testing.T) { +tempDir := t.TempDir() +certPath := filepath.Join(tempDir, "test-cert.crt") +testCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") + +err := os.WriteFile(certPath, testCert, 0644) +assert.NoError(t, err) + +// Verify file was created and contains correct data +readCert, err := os.ReadFile(certPath) +assert.NoError(t, err) +assert.Equal(t, testCert, readCert) + +// Verify file permissions +info, err := os.Stat(certPath) +assert.NoError(t, err) +assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) +}) + +t.Run("create docker certs.d directory structure", func(t *testing.T) { +tempDir := t.TempDir() +registryHost := "gitea.cnoe.localtest.me:8443" +dockerCertsDir := filepath.Join(tempDir, ".docker", "certs.d", registryHost) + +err := os.MkdirAll(dockerCertsDir, 0755) +assert.NoError(t, err) + +// Verify directory was created +info, err := os.Stat(dockerCertsDir) +assert.NoError(t, err) +assert.True(t, info.IsDir()) + +// Verify we can write a certificate to it +certPath := filepath.Join(dockerCertsDir, "ca.crt") +testCert := []byte("test certificate") +err = os.WriteFile(certPath, testCert, 0644) +assert.NoError(t, err) + +readCert, err := os.ReadFile(certPath) +assert.NoError(t, err) +assert.Equal(t, testCert, readCert) +}) + +t.Run("handle directory creation errors", func(t *testing.T) { +// Try to create a directory in a non-existent parent that we can't create +invalidPath := filepath.Join("/nonexistent-root-12345", "subdir") +err := os.MkdirAll(invalidPath, 0755) +assert.Error(t, err) +}) +} + +func TestPrintPostInstallInstructions(t *testing.T) { +// This is a smoke test to ensure the function doesn't panic +// We don't assert on the output as it's informational +t.Run("print instructions without panic", func(t *testing.T) { +assert.NotPanics(t, func() { +printPostInstallInstructions("gitea.cnoe.localtest.me:8443") +}) +}) +} + +func TestCertificateCommandFlags(t *testing.T) { +t.Run("certificate command has required flags", func(t *testing.T) { +assert.NotNil(t, CertificateCmd) +assert.Equal(t, "certificate", CertificateCmd.Use) + +// Check that flags are defined +outputFlag := CertificateCmd.Flags().Lookup("output") +assert.NotNil(t, outputFlag) +assert.Equal(t, "o", outputFlag.Shorthand) + +dockerFlag := CertificateCmd.Flags().Lookup("docker") +assert.NotNil(t, dockerFlag) +assert.Equal(t, "false", dockerFlag.DefValue) +}) +} diff --git a/pkg/cmd/get/root.go b/pkg/cmd/get/root.go index 128985173..e69839e0f 100644 --- a/pkg/cmd/get/root.go +++ b/pkg/cmd/get/root.go @@ -22,6 +22,7 @@ func init() { GetCmd.AddCommand(ClustersCmd) GetCmd.AddCommand(SecretsCmd) GetCmd.AddCommand(PackagesCmd) + GetCmd.AddCommand(CertificateCmd) GetCmd.PersistentFlags().StringSliceVarP(&packages, "packages", "p", []string{}, "names of packages.") GetCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", "Output format: table (default if not specified), json or yaml.") GetCmd.PersistentFlags().StringVarP(&util.KubeConfigPath, "kubeconfig", "", "", "kube config file Path.") From 42732ae3f3074792dc3e6c51227dbcb710e39491 Mon Sep 17 00:00:00 2001 From: Greg Haynes Date: Fri, 19 Dec 2025 17:00:00 +0000 Subject: [PATCH 2/3] Fix formatting Signed-off-by: Greg Haynes --- pkg/cmd/get/certificate.go | 280 +++++++++++----------- pkg/cmd/get/certificate_test.go | 400 ++++++++++++++++---------------- 2 files changed, 340 insertions(+), 340 deletions(-) diff --git a/pkg/cmd/get/certificate.go b/pkg/cmd/get/certificate.go index 32e44ea49..e18e5e11c 100644 --- a/pkg/cmd/get/certificate.go +++ b/pkg/cmd/get/certificate.go @@ -1,28 +1,28 @@ package get import ( -"context" -"fmt" -"os" -"path/filepath" -"runtime" - -"github.com/cnoe-io/idpbuilder/globals" -"github.com/cnoe-io/idpbuilder/pkg/util" -"github.com/spf13/cobra" -corev1 "k8s.io/api/core/v1" -"sigs.k8s.io/controller-runtime/pkg/client" + "context" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/cnoe-io/idpbuilder/globals" + "github.com/cnoe-io/idpbuilder/pkg/util" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ) var ( -certOutputPath string -setupDocker bool + certOutputPath string + setupDocker bool ) var CertificateCmd = &cobra.Command{ -Use: "certificate", -Short: "Export the TLS certificate from the cluster", -Long: `Export the self-signed TLS certificate from the cluster. + Use: "certificate", + Short: "Export the TLS certificate from the cluster", + Long: `Export the self-signed TLS certificate from the cluster. By default, the certificate is printed to stdout. Use the --docker flag to automatically configure Docker's per-registry certificate directory, which allows Docker to trust the @@ -37,143 +37,143 @@ Examples: # Export to a custom file idpbuilder get certificate --output ~/my-cert.crt`, -RunE: getCertificateE, -SilenceUsage: true, + RunE: getCertificateE, + SilenceUsage: true, } func init() { -CertificateCmd.Flags().StringVarP(&certOutputPath, "output", "o", "", "Custom output path for the certificate file") -CertificateCmd.Flags().BoolVar(&setupDocker, "docker", false, "Setup Docker registry certificate directory") + CertificateCmd.Flags().StringVarP(&certOutputPath, "output", "o", "", "Custom output path for the certificate file") + CertificateCmd.Flags().BoolVar(&setupDocker, "docker", false, "Setup Docker registry certificate directory") } func getCertificateE(cmd *cobra.Command, args []string) error { -ctx, ctxCancel := context.WithCancel(cmd.Context()) -defer ctxCancel() - -kubeConfig, err := util.GetKubeConfig() -if err != nil { -return fmt.Errorf("getting kube config: %w", err) -} - -kubeClient, err := util.GetKubeClient(kubeConfig) -if err != nil { -return fmt.Errorf("getting kube client: %w", err) -} - -// Get the certificate from the cluster -cert, err := getCertificateFromCluster(ctx, kubeClient) -if err != nil { -return err -} - -// Get the build configuration to determine registry host -config, err := util.GetConfig(ctx) -if err != nil { -return fmt.Errorf("getting idp config: %w", err) -} - -var registryHost string -if config.UsePathRouting { -registryHost = fmt.Sprintf("%s:%s", config.Host, config.Port) -} else { -registryHost = fmt.Sprintf("gitea.%s:%s", config.Host, config.Port) -} - -// Determine output behavior -if certOutputPath != "" { -// Custom output path specified -if err := os.WriteFile(certOutputPath, cert, 0644); err != nil { -return fmt.Errorf("writing certificate to %s: %w", certOutputPath, err) -} -fmt.Printf("Certificate exported to: %s\n", certOutputPath) -return nil -} else if setupDocker { -// Docker's per-registry certificate directory -homeDir, err := os.UserHomeDir() -if err != nil { -return fmt.Errorf("getting user home directory: %w", err) -} - -dockerCertsDir := filepath.Join(homeDir, ".docker", "certs.d", registryHost) -if err := os.MkdirAll(dockerCertsDir, 0755); err != nil { -return fmt.Errorf("creating Docker certificate directory: %w", err) -} - -outputPath := filepath.Join(dockerCertsDir, "ca.crt") -if err := os.WriteFile(outputPath, cert, 0644); err != nil { -return fmt.Errorf("writing certificate to %s: %w", outputPath, err) -} - -fmt.Printf("Certificate exported successfully to: %s\n", outputPath) -fmt.Printf("Registry host: %s\n\n", registryHost) -printPostInstallInstructions(registryHost) -return nil -} - -// Default: print to stdout -fmt.Print(string(cert)) -return nil + ctx, ctxCancel := context.WithCancel(cmd.Context()) + defer ctxCancel() + + kubeConfig, err := util.GetKubeConfig() + if err != nil { + return fmt.Errorf("getting kube config: %w", err) + } + + kubeClient, err := util.GetKubeClient(kubeConfig) + if err != nil { + return fmt.Errorf("getting kube client: %w", err) + } + + // Get the certificate from the cluster + cert, err := getCertificateFromCluster(ctx, kubeClient) + if err != nil { + return err + } + + // Get the build configuration to determine registry host + config, err := util.GetConfig(ctx) + if err != nil { + return fmt.Errorf("getting idp config: %w", err) + } + + var registryHost string + if config.UsePathRouting { + registryHost = fmt.Sprintf("%s:%s", config.Host, config.Port) + } else { + registryHost = fmt.Sprintf("gitea.%s:%s", config.Host, config.Port) + } + + // Determine output behavior + if certOutputPath != "" { + // Custom output path specified + if err := os.WriteFile(certOutputPath, cert, 0644); err != nil { + return fmt.Errorf("writing certificate to %s: %w", certOutputPath, err) + } + fmt.Printf("Certificate exported to: %s\n", certOutputPath) + return nil + } else if setupDocker { + // Docker's per-registry certificate directory + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("getting user home directory: %w", err) + } + + dockerCertsDir := filepath.Join(homeDir, ".docker", "certs.d", registryHost) + if err := os.MkdirAll(dockerCertsDir, 0755); err != nil { + return fmt.Errorf("creating Docker certificate directory: %w", err) + } + + outputPath := filepath.Join(dockerCertsDir, "ca.crt") + if err := os.WriteFile(outputPath, cert, 0644); err != nil { + return fmt.Errorf("writing certificate to %s: %w", outputPath, err) + } + + fmt.Printf("Certificate exported successfully to: %s\n", outputPath) + fmt.Printf("Registry host: %s\n\n", registryHost) + printPostInstallInstructions(registryHost) + return nil + } + + // Default: print to stdout + fmt.Print(string(cert)) + return nil } func getCertificateFromCluster(ctx context.Context, kubeClient client.Client) ([]byte, error) { -secret := &corev1.Secret{} -secretKey := client.ObjectKey{ -Name: globals.SelfSignedCertSecretName, -Namespace: globals.NginxNamespace, -} + secret := &corev1.Secret{} + secretKey := client.ObjectKey{ + Name: globals.SelfSignedCertSecretName, + Namespace: globals.NginxNamespace, + } -if err := kubeClient.Get(ctx, secretKey, secret); err != nil { -return nil, fmt.Errorf("getting certificate secret from cluster: %w. Make sure the cluster is running.", err) -} + if err := kubeClient.Get(ctx, secretKey, secret); err != nil { + return nil, fmt.Errorf("getting certificate secret from cluster: %w. Make sure the cluster is running.", err) + } -cert, ok := secret.Data[corev1.TLSCertKey] -if !ok { -return nil, fmt.Errorf("certificate not found in secret %s/%s", globals.NginxNamespace, globals.SelfSignedCertSecretName) -} + cert, ok := secret.Data[corev1.TLSCertKey] + if !ok { + return nil, fmt.Errorf("certificate not found in secret %s/%s", globals.NginxNamespace, globals.SelfSignedCertSecretName) + } -return cert, nil + return cert, nil } func printPostInstallInstructions(registryHost string) { -fmt.Println("Next steps:") -fmt.Println(" 1. The certificate has been configured for Docker's per-registry trust") -fmt.Println(" 2. No Docker restart is required - the certificate should work immediately") -fmt.Println() -fmt.Println("To verify Docker can access the registry:") -fmt.Printf(" docker pull %s/test-image\n", registryHost) -fmt.Println() - -// Platform-specific instructions for system-wide trust (optional) -switch runtime.GOOS { -case "darwin": -fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") -fmt.Println(" 1. Find the certificate file:") -homeDir, _ := os.UserHomeDir() -fmt.Printf(" %s\n", filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt")) -fmt.Println(" 2. Double-click the certificate file to open Keychain Access") -fmt.Println(" 3. Select 'System' keychain and click 'Add'") -fmt.Println(" 4. Double-click the imported certificate") -fmt.Println(" 5. Expand 'Trust' section and set 'When using this certificate' to 'Always Trust'") -fmt.Println() -fmt.Println(" Or use the command line:") -certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") -fmt.Printf(" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain %s\n", certPath) - -case "linux": -fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") -homeDir, _ := os.UserHomeDir() -certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") -fmt.Printf(" sudo cp %s /usr/local/share/ca-certificates/idpbuilder-ca.crt\n", certPath) -fmt.Println(" sudo update-ca-certificates") - -case "windows": -fmt.Println("Optional: To trust this certificate system-wide:") -fmt.Println(" 1. Open the certificate file in Explorer") -fmt.Println(" 2. Click 'Install Certificate'") -fmt.Println(" 3. Select 'Local Machine' and click Next") -fmt.Println(" 4. Select 'Place all certificates in the following store'") -fmt.Println(" 5. Browse to 'Trusted Root Certification Authorities'") -fmt.Println(" 6. Click OK and Finish") -} -fmt.Println() + fmt.Println("Next steps:") + fmt.Println(" 1. The certificate has been configured for Docker's per-registry trust") + fmt.Println(" 2. No Docker restart is required - the certificate should work immediately") + fmt.Println() + fmt.Println("To verify Docker can access the registry:") + fmt.Printf(" docker pull %s/test-image\n", registryHost) + fmt.Println() + + // Platform-specific instructions for system-wide trust (optional) + switch runtime.GOOS { + case "darwin": + fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") + fmt.Println(" 1. Find the certificate file:") + homeDir, _ := os.UserHomeDir() + fmt.Printf(" %s\n", filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt")) + fmt.Println(" 2. Double-click the certificate file to open Keychain Access") + fmt.Println(" 3. Select 'System' keychain and click 'Add'") + fmt.Println(" 4. Double-click the imported certificate") + fmt.Println(" 5. Expand 'Trust' section and set 'When using this certificate' to 'Always Trust'") + fmt.Println() + fmt.Println(" Or use the command line:") + certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") + fmt.Printf(" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain %s\n", certPath) + + case "linux": + fmt.Println("Optional: To trust this certificate system-wide (for browsers, curl, etc.):") + homeDir, _ := os.UserHomeDir() + certPath := filepath.Join(homeDir, ".docker", "certs.d", registryHost, "ca.crt") + fmt.Printf(" sudo cp %s /usr/local/share/ca-certificates/idpbuilder-ca.crt\n", certPath) + fmt.Println(" sudo update-ca-certificates") + + case "windows": + fmt.Println("Optional: To trust this certificate system-wide:") + fmt.Println(" 1. Open the certificate file in Explorer") + fmt.Println(" 2. Click 'Install Certificate'") + fmt.Println(" 3. Select 'Local Machine' and click Next") + fmt.Println(" 4. Select 'Place all certificates in the following store'") + fmt.Println(" 5. Browse to 'Trusted Root Certification Authorities'") + fmt.Println(" 6. Click OK and Finish") + } + fmt.Println() } diff --git a/pkg/cmd/get/certificate_test.go b/pkg/cmd/get/certificate_test.go index c925d3088..411cff9b4 100644 --- a/pkg/cmd/get/certificate_test.go +++ b/pkg/cmd/get/certificate_test.go @@ -1,219 +1,219 @@ package get import ( -"context" -"os" -"path/filepath" -"testing" - -"github.com/cnoe-io/idpbuilder/api/v1alpha1" -"github.com/cnoe-io/idpbuilder/globals" -"github.com/stretchr/testify/assert" -"github.com/stretchr/testify/mock" -corev1 "k8s.io/api/core/v1" -"k8s.io/apimachinery/pkg/api/errors" -"k8s.io/apimachinery/pkg/runtime/schema" -"sigs.k8s.io/controller-runtime/pkg/client" + "context" + "os" + "path/filepath" + "testing" + + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + "github.com/cnoe-io/idpbuilder/globals" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" ) func TestGetCertificateFromCluster(t *testing.T) { -ctx := context.Background() - -t.Run("successful certificate retrieval", func(t *testing.T) { -fClient := new(fakeKubeClient) -expectedCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") - -secretKey := client.ObjectKey{ -Name: globals.SelfSignedCertSecretName, -Namespace: globals.NginxNamespace, -} - -fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { -arg := args.Get(2).(*corev1.Secret) -arg.Data = map[string][]byte{ -corev1.TLSCertKey: expectedCert, -} -}).Return(nil) - -cert, err := getCertificateFromCluster(ctx, fClient) -assert.NoError(t, err) -assert.Equal(t, expectedCert, cert) -fClient.AssertExpectations(t) -}) - -t.Run("secret not found", func(t *testing.T) { -fClient := new(fakeKubeClient) - -secretKey := client.ObjectKey{ -Name: globals.SelfSignedCertSecretName, -Namespace: globals.NginxNamespace, -} - -notFoundErr := errors.NewNotFound(schema.GroupResource{Resource: "secrets"}, globals.SelfSignedCertSecretName) -fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Return(notFoundErr) - -cert, err := getCertificateFromCluster(ctx, fClient) -assert.Error(t, err) -assert.Nil(t, cert) -assert.Contains(t, err.Error(), "getting certificate secret from cluster") -fClient.AssertExpectations(t) -}) - -t.Run("certificate data missing from secret", func(t *testing.T) { -fClient := new(fakeKubeClient) - -secretKey := client.ObjectKey{ -Name: globals.SelfSignedCertSecretName, -Namespace: globals.NginxNamespace, -} - -fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { -arg := args.Get(2).(*corev1.Secret) -arg.Data = map[string][]byte{ -// Missing TLSCertKey -"other-key": []byte("value"), -} -}).Return(nil) - -cert, err := getCertificateFromCluster(ctx, fClient) -assert.Error(t, err) -assert.Nil(t, cert) -assert.Contains(t, err.Error(), "certificate not found in secret") -fClient.AssertExpectations(t) -}) + ctx := context.Background() + + t.Run("successful certificate retrieval", func(t *testing.T) { + fClient := new(fakeKubeClient) + expectedCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") + + secretKey := client.ObjectKey{ + Name: globals.SelfSignedCertSecretName, + Namespace: globals.NginxNamespace, + } + + fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + arg := args.Get(2).(*corev1.Secret) + arg.Data = map[string][]byte{ + corev1.TLSCertKey: expectedCert, + } + }).Return(nil) + + cert, err := getCertificateFromCluster(ctx, fClient) + assert.NoError(t, err) + assert.Equal(t, expectedCert, cert) + fClient.AssertExpectations(t) + }) + + t.Run("secret not found", func(t *testing.T) { + fClient := new(fakeKubeClient) + + secretKey := client.ObjectKey{ + Name: globals.SelfSignedCertSecretName, + Namespace: globals.NginxNamespace, + } + + notFoundErr := errors.NewNotFound(schema.GroupResource{Resource: "secrets"}, globals.SelfSignedCertSecretName) + fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Return(notFoundErr) + + cert, err := getCertificateFromCluster(ctx, fClient) + assert.Error(t, err) + assert.Nil(t, cert) + assert.Contains(t, err.Error(), "getting certificate secret from cluster") + fClient.AssertExpectations(t) + }) + + t.Run("certificate data missing from secret", func(t *testing.T) { + fClient := new(fakeKubeClient) + + secretKey := client.ObjectKey{ + Name: globals.SelfSignedCertSecretName, + Namespace: globals.NginxNamespace, + } + + fClient.On("Get", ctx, secretKey, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + arg := args.Get(2).(*corev1.Secret) + arg.Data = map[string][]byte{ + // Missing TLSCertKey + "other-key": []byte("value"), + } + }).Return(nil) + + cert, err := getCertificateFromCluster(ctx, fClient) + assert.Error(t, err) + assert.Nil(t, cert) + assert.Contains(t, err.Error(), "certificate not found in secret") + fClient.AssertExpectations(t) + }) } func TestRegistryHostDetermination(t *testing.T) { -testCases := []struct { -name string -config v1alpha1.BuildCustomizationSpec -expectedHost string -}{ -{ -name: "subdomain routing", -config: v1alpha1.BuildCustomizationSpec{ -Host: "cnoe.localtest.me", -Port: "8443", -UsePathRouting: false, -}, -expectedHost: "gitea.cnoe.localtest.me:8443", -}, -{ -name: "path routing", -config: v1alpha1.BuildCustomizationSpec{ -Host: "cnoe.localtest.me", -Port: "8443", -UsePathRouting: true, -}, -expectedHost: "cnoe.localtest.me:8443", -}, -{ -name: "custom host with path routing", -config: v1alpha1.BuildCustomizationSpec{ -Host: "example.com", -Port: "443", -UsePathRouting: true, -}, -expectedHost: "example.com:443", -}, -{ -name: "custom host with subdomain routing", -config: v1alpha1.BuildCustomizationSpec{ -Host: "example.com", -Port: "443", -UsePathRouting: false, -}, -expectedHost: "gitea.example.com:443", -}, -} - -for _, tc := range testCases { -t.Run(tc.name, func(t *testing.T) { -var registryHost string -if tc.config.UsePathRouting { -registryHost = tc.config.Host + ":" + tc.config.Port -} else { -registryHost = "gitea." + tc.config.Host + ":" + tc.config.Port -} -assert.Equal(t, tc.expectedHost, registryHost) -}) -} + testCases := []struct { + name string + config v1alpha1.BuildCustomizationSpec + expectedHost string + }{ + { + name: "subdomain routing", + config: v1alpha1.BuildCustomizationSpec{ + Host: "cnoe.localtest.me", + Port: "8443", + UsePathRouting: false, + }, + expectedHost: "gitea.cnoe.localtest.me:8443", + }, + { + name: "path routing", + config: v1alpha1.BuildCustomizationSpec{ + Host: "cnoe.localtest.me", + Port: "8443", + UsePathRouting: true, + }, + expectedHost: "cnoe.localtest.me:8443", + }, + { + name: "custom host with path routing", + config: v1alpha1.BuildCustomizationSpec{ + Host: "example.com", + Port: "443", + UsePathRouting: true, + }, + expectedHost: "example.com:443", + }, + { + name: "custom host with subdomain routing", + config: v1alpha1.BuildCustomizationSpec{ + Host: "example.com", + Port: "443", + UsePathRouting: false, + }, + expectedHost: "gitea.example.com:443", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var registryHost string + if tc.config.UsePathRouting { + registryHost = tc.config.Host + ":" + tc.config.Port + } else { + registryHost = "gitea." + tc.config.Host + ":" + tc.config.Port + } + assert.Equal(t, tc.expectedHost, registryHost) + }) + } } func TestCertificateFileOperations(t *testing.T) { -t.Run("write certificate to custom path", func(t *testing.T) { -tempDir := t.TempDir() -certPath := filepath.Join(tempDir, "test-cert.crt") -testCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") - -err := os.WriteFile(certPath, testCert, 0644) -assert.NoError(t, err) - -// Verify file was created and contains correct data -readCert, err := os.ReadFile(certPath) -assert.NoError(t, err) -assert.Equal(t, testCert, readCert) - -// Verify file permissions -info, err := os.Stat(certPath) -assert.NoError(t, err) -assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) -}) - -t.Run("create docker certs.d directory structure", func(t *testing.T) { -tempDir := t.TempDir() -registryHost := "gitea.cnoe.localtest.me:8443" -dockerCertsDir := filepath.Join(tempDir, ".docker", "certs.d", registryHost) - -err := os.MkdirAll(dockerCertsDir, 0755) -assert.NoError(t, err) - -// Verify directory was created -info, err := os.Stat(dockerCertsDir) -assert.NoError(t, err) -assert.True(t, info.IsDir()) - -// Verify we can write a certificate to it -certPath := filepath.Join(dockerCertsDir, "ca.crt") -testCert := []byte("test certificate") -err = os.WriteFile(certPath, testCert, 0644) -assert.NoError(t, err) - -readCert, err := os.ReadFile(certPath) -assert.NoError(t, err) -assert.Equal(t, testCert, readCert) -}) - -t.Run("handle directory creation errors", func(t *testing.T) { -// Try to create a directory in a non-existent parent that we can't create -invalidPath := filepath.Join("/nonexistent-root-12345", "subdir") -err := os.MkdirAll(invalidPath, 0755) -assert.Error(t, err) -}) + t.Run("write certificate to custom path", func(t *testing.T) { + tempDir := t.TempDir() + certPath := filepath.Join(tempDir, "test-cert.crt") + testCert := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") + + err := os.WriteFile(certPath, testCert, 0644) + assert.NoError(t, err) + + // Verify file was created and contains correct data + readCert, err := os.ReadFile(certPath) + assert.NoError(t, err) + assert.Equal(t, testCert, readCert) + + // Verify file permissions + info, err := os.Stat(certPath) + assert.NoError(t, err) + assert.Equal(t, os.FileMode(0644), info.Mode().Perm()) + }) + + t.Run("create docker certs.d directory structure", func(t *testing.T) { + tempDir := t.TempDir() + registryHost := "gitea.cnoe.localtest.me:8443" + dockerCertsDir := filepath.Join(tempDir, ".docker", "certs.d", registryHost) + + err := os.MkdirAll(dockerCertsDir, 0755) + assert.NoError(t, err) + + // Verify directory was created + info, err := os.Stat(dockerCertsDir) + assert.NoError(t, err) + assert.True(t, info.IsDir()) + + // Verify we can write a certificate to it + certPath := filepath.Join(dockerCertsDir, "ca.crt") + testCert := []byte("test certificate") + err = os.WriteFile(certPath, testCert, 0644) + assert.NoError(t, err) + + readCert, err := os.ReadFile(certPath) + assert.NoError(t, err) + assert.Equal(t, testCert, readCert) + }) + + t.Run("handle directory creation errors", func(t *testing.T) { + // Try to create a directory in a non-existent parent that we can't create + invalidPath := filepath.Join("/nonexistent-root-12345", "subdir") + err := os.MkdirAll(invalidPath, 0755) + assert.Error(t, err) + }) } func TestPrintPostInstallInstructions(t *testing.T) { -// This is a smoke test to ensure the function doesn't panic -// We don't assert on the output as it's informational -t.Run("print instructions without panic", func(t *testing.T) { -assert.NotPanics(t, func() { -printPostInstallInstructions("gitea.cnoe.localtest.me:8443") -}) -}) + // This is a smoke test to ensure the function doesn't panic + // We don't assert on the output as it's informational + t.Run("print instructions without panic", func(t *testing.T) { + assert.NotPanics(t, func() { + printPostInstallInstructions("gitea.cnoe.localtest.me:8443") + }) + }) } func TestCertificateCommandFlags(t *testing.T) { -t.Run("certificate command has required flags", func(t *testing.T) { -assert.NotNil(t, CertificateCmd) -assert.Equal(t, "certificate", CertificateCmd.Use) - -// Check that flags are defined -outputFlag := CertificateCmd.Flags().Lookup("output") -assert.NotNil(t, outputFlag) -assert.Equal(t, "o", outputFlag.Shorthand) - -dockerFlag := CertificateCmd.Flags().Lookup("docker") -assert.NotNil(t, dockerFlag) -assert.Equal(t, "false", dockerFlag.DefValue) -}) + t.Run("certificate command has required flags", func(t *testing.T) { + assert.NotNil(t, CertificateCmd) + assert.Equal(t, "certificate", CertificateCmd.Use) + + // Check that flags are defined + outputFlag := CertificateCmd.Flags().Lookup("output") + assert.NotNil(t, outputFlag) + assert.Equal(t, "o", outputFlag.Shorthand) + + dockerFlag := CertificateCmd.Flags().Lookup("docker") + assert.NotNil(t, dockerFlag) + assert.Equal(t, "false", dockerFlag.DefValue) + }) } From ffff76591944ce6c58e2fbaea2cfbbb31efbcf8c Mon Sep 17 00:00:00 2001 From: Greg Haynes Date: Fri, 19 Dec 2025 17:08:39 +0000 Subject: [PATCH 3/3] Update controller-tools to v0.20.0 for Go 1.25 compatibility Signed-off-by: Greg Haynes --- Makefile | 2 +- pkg/cmd/get/root.go | 2 +- .../resources/idpbuilder.cnoe.io_custompackages.yaml | 2 +- .../resources/idpbuilder.cnoe.io_gitrepositories.yaml | 2 +- pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 67e6c1279..6cb852913 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ HELM_TGZ ?= $(LOCALBIN)/helm.tar.gz HELM ?= $(LOCALBIN)/helm ## Tool Versions -CONTROLLER_TOOLS_VERSION ?= v0.15.0 +CONTROLLER_TOOLS_VERSION ?= v0.20.0 .PHONY: fmt fmt: ## Run go fmt against code. diff --git a/pkg/cmd/get/root.go b/pkg/cmd/get/root.go index e69839e0f..612f2f7bd 100644 --- a/pkg/cmd/get/root.go +++ b/pkg/cmd/get/root.go @@ -22,7 +22,7 @@ func init() { GetCmd.AddCommand(ClustersCmd) GetCmd.AddCommand(SecretsCmd) GetCmd.AddCommand(PackagesCmd) - GetCmd.AddCommand(CertificateCmd) + GetCmd.AddCommand(CertificateCmd) GetCmd.PersistentFlags().StringSliceVarP(&packages, "packages", "p", []string{}, "names of packages.") GetCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", "Output format: table (default if not specified), json or yaml.") GetCmd.PersistentFlags().StringVarP(&util.KubeConfigPath, "kubeconfig", "", "", "kube config file Path.") diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml index a81df9ad8..805eb2c9f 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_custompackages.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.20.0 name: custompackages.idpbuilder.cnoe.io spec: group: idpbuilder.cnoe.io diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_gitrepositories.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_gitrepositories.yaml index e305cdcf3..ed17fbf42 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_gitrepositories.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_gitrepositories.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.20.0 name: gitrepositories.idpbuilder.cnoe.io spec: group: idpbuilder.cnoe.io diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml index d1959ab7c..e9bfcc9af 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.15.0 + controller-gen.kubebuilder.io/version: v0.20.0 name: localbuilds.idpbuilder.cnoe.io spec: group: idpbuilder.cnoe.io