Skip to content
Closed
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
179 changes: 179 additions & 0 deletions pkg/cmd/get/certificate.go
Original file line number Diff line number Diff line change
@@ -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()
}
219 changes: 219 additions & 0 deletions pkg/cmd/get/certificate_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading
Loading