From e9a18814671eca27c532ae5b05f6151098230c16 Mon Sep 17 00:00:00 2001 From: Dasmat13 Date: Mon, 6 Jul 2026 20:20:24 +0530 Subject: [PATCH] feat(cli): add --external-certificate flag to oc create route edge/reencrypt Add support for spec.tls.externalCertificate (GA in OCP 4.19) to both 'oc create route edge' and 'oc create route reencrypt' commands. This allows users to reference a kubernetes.io/tls Secret by name at route creation time, enabling integration with external certificate managers like cert-manager, without resorting to post-creation patching. Behavior: - --external-certificate= sets spec.tls.externalCertificate - Mutually exclusive with --cert and --key (returns error if combined) - Passthrough routes are out of scope (no TLS termination at router) Closes: #2254 Signed-off-by: Dasmat13 --- pkg/cli/create/route_test.go | 138 ++++++++++++++++++++++++++++++ pkg/cli/create/routeedge.go | 66 +++++++++----- pkg/cli/create/routereenecrypt.go | 68 +++++++++------ 3 files changed, 225 insertions(+), 47 deletions(-) create mode 100644 pkg/cli/create/route_test.go diff --git a/pkg/cli/create/route_test.go b/pkg/cli/create/route_test.go new file mode 100644 index 0000000000..0d9f316c9c --- /dev/null +++ b/pkg/cli/create/route_test.go @@ -0,0 +1,138 @@ +package create + +import ( + "strings" + "testing" +) + +func TestCreateEdgeRouteValidate(t *testing.T) { + tests := []struct { + name string + options CreateEdgeRouteOptions + expectedError string + }{ + { + name: "valid options without external certificate", + options: CreateEdgeRouteOptions{ + Cert: "cert-path", + Key: "key-path", + CACert: "ca-cert-path", + }, + }, + { + name: "valid options with external certificate only", + options: CreateEdgeRouteOptions{ + ExternalCertificate: "my-tls-secret", + }, + }, + { + name: "invalid options: external certificate with cert", + options: CreateEdgeRouteOptions{ + ExternalCertificate: "my-tls-secret", + Cert: "cert-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + { + name: "invalid options: external certificate with key", + options: CreateEdgeRouteOptions{ + ExternalCertificate: "my-tls-secret", + Key: "key-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + { + name: "invalid options: external certificate with ca-cert", + options: CreateEdgeRouteOptions{ + ExternalCertificate: "my-tls-secret", + CACert: "ca-cert-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.options.Validate() + if len(tc.expectedError) > 0 { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.expectedError) + } + if !strings.Contains(err.Error(), tc.expectedError) { + t.Fatalf("expected error containing %q, got: %v", tc.expectedError, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + }) + } +} + +func TestCreateReencryptRouteValidate(t *testing.T) { + tests := []struct { + name string + options CreateReencryptRouteOptions + expectedError string + }{ + { + name: "valid options without external certificate", + options: CreateReencryptRouteOptions{ + Cert: "cert-path", + Key: "key-path", + CACert: "ca-cert-path", + DestCACert: "dest-ca-cert-path", + }, + }, + { + name: "valid options with external certificate and dest ca-cert", + options: CreateReencryptRouteOptions{ + ExternalCertificate: "my-tls-secret", + DestCACert: "dest-ca-cert-path", + }, + }, + { + name: "invalid options: external certificate with cert", + options: CreateReencryptRouteOptions{ + ExternalCertificate: "my-tls-secret", + Cert: "cert-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + { + name: "invalid options: external certificate with key", + options: CreateReencryptRouteOptions{ + ExternalCertificate: "my-tls-secret", + Key: "key-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + { + name: "invalid options: external certificate with ca-cert", + options: CreateReencryptRouteOptions{ + ExternalCertificate: "my-tls-secret", + CACert: "ca-cert-path", + }, + expectedError: "--external-certificate is mutually exclusive with --cert, --key, and --ca-cert", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.options.Validate() + if len(tc.expectedError) > 0 { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.expectedError) + } + if !strings.Contains(err.Error(), tc.expectedError) { + t.Fatalf("expected error containing %q, got: %v", tc.expectedError, err) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + }) + } +} diff --git a/pkg/cli/create/routeedge.go b/pkg/cli/create/routeedge.go index 1a9d741e2e..533d08e592 100644 --- a/pkg/cli/create/routeedge.go +++ b/pkg/cli/create/routeedge.go @@ -34,21 +34,25 @@ var ( # Create an edge route that exposes the frontend service and specify a path # If the route name is omitted, the service name will be used oc create route edge --service=frontend --path /assets + + # Create an edge route referencing an external TLS secret (cert-manager, etc.) + oc create route edge my-route --service=frontend --external-certificate=my-tls-secret `) ) type CreateEdgeRouteOptions struct { CreateRouteSubcommandOptions *CreateRouteSubcommandOptions - Hostname string - Port string - InsecurePolicy string - Service string - Path string - Cert string - Key string - CACert string - WildcardPolicy string + Hostname string + Port string + InsecurePolicy string + Service string + Path string + Cert string + Key string + CACert string + WildcardPolicy string + ExternalCertificate string } // NewCmdCreateEdgeRoute is a macro command to create an edge route. @@ -63,6 +67,7 @@ func NewCmdCreateEdgeRoute(f kcmdutil.Factory, streams genericiooptions.IOStream Example: edgeRouteExample, Run: func(cmd *cobra.Command, args []string) { kcmdutil.CheckErr(o.Complete(f, cmd, args)) + kcmdutil.CheckErr(o.Validate()) kcmdutil.CheckErr(o.Run()) }, } @@ -80,6 +85,7 @@ func NewCmdCreateEdgeRoute(f kcmdutil.Factory, streams genericiooptions.IOStream cmd.Flags().StringVar(&o.CACert, "ca-cert", o.CACert, "Path to a CA certificate file.") cmd.MarkFlagFilename("ca-cert") cmd.Flags().StringVar(&o.WildcardPolicy, "wildcard-policy", o.WildcardPolicy, "Sets the WilcardPolicy for the hostname, the default is \"None\". valid values are \"None\" and \"Subdomain\"") + cmd.Flags().StringVar(&o.ExternalCertificate, "external-certificate", o.ExternalCertificate, "Name of a Secret (kubernetes.io/tls) in the same namespace to use as the router certificate. Mutually exclusive with --cert and --key.") kcmdutil.AddValidateFlags(cmd) o.CreateRouteSubcommandOptions.AddFlags(cmd) @@ -92,6 +98,15 @@ func (o *CreateEdgeRouteOptions) Complete(f kcmdutil.Factory, cmd *cobra.Command return o.CreateRouteSubcommandOptions.Complete(f, cmd, args) } +func (o *CreateEdgeRouteOptions) Validate() error { + if len(o.ExternalCertificate) > 0 { + if len(o.Cert) > 0 || len(o.Key) > 0 || len(o.CACert) > 0 { + return fmt.Errorf("--external-certificate is mutually exclusive with --cert, --key, and --ca-cert") + } + } + return nil +} + func (o *CreateEdgeRouteOptions) Run() error { serviceName, err := resolveServiceName(o.CreateRouteSubcommandOptions.Mapper, o.Service) if err != nil { @@ -111,21 +126,26 @@ func (o *CreateEdgeRouteOptions) Run() error { route.Spec.TLS = new(routev1.TLSConfig) route.Spec.TLS.Termination = routev1.TLSTerminationEdge - cert, err := fileutil.LoadData(o.Cert) - if err != nil { - return err - } - route.Spec.TLS.Certificate = string(cert) - key, err := fileutil.LoadData(o.Key) - if err != nil { - return err - } - route.Spec.TLS.Key = string(key) - caCert, err := fileutil.LoadData(o.CACert) - if err != nil { - return err + + if len(o.ExternalCertificate) > 0 { + route.Spec.TLS.ExternalCertificate = &routev1.LocalObjectReference{Name: o.ExternalCertificate} + } else { + cert, err := fileutil.LoadData(o.Cert) + if err != nil { + return err + } + route.Spec.TLS.Certificate = string(cert) + key, err := fileutil.LoadData(o.Key) + if err != nil { + return err + } + route.Spec.TLS.Key = string(key) + caCert, err := fileutil.LoadData(o.CACert) + if err != nil { + return err + } + route.Spec.TLS.CACertificate = string(caCert) } - route.Spec.TLS.CACertificate = string(caCert) if len(o.InsecurePolicy) > 0 { route.Spec.TLS.InsecureEdgeTerminationPolicy = routev1.InsecureEdgeTerminationPolicyType(o.InsecurePolicy) diff --git a/pkg/cli/create/routereenecrypt.go b/pkg/cli/create/routereenecrypt.go index 9425d881b2..94c7eebf84 100644 --- a/pkg/cli/create/routereenecrypt.go +++ b/pkg/cli/create/routereenecrypt.go @@ -2,6 +2,7 @@ package create import ( "context" + "fmt" "github.com/spf13/cobra" @@ -34,22 +35,26 @@ var ( # route name default to the service name and the destination CA certificate # default to the service CA oc create route reencrypt --service=frontend + + # Create a reencrypt route referencing an external TLS secret (cert-manager, etc.) + oc create route reencrypt my-route --service=frontend --external-certificate=my-tls-secret `) ) type CreateReencryptRouteOptions struct { CreateRouteSubcommandOptions *CreateRouteSubcommandOptions - Hostname string - Port string - InsecurePolicy string - Service string - Path string - Cert string - Key string - CACert string - DestCACert string - WildcardPolicy string + Hostname string + Port string + InsecurePolicy string + Service string + Path string + Cert string + Key string + CACert string + DestCACert string + WildcardPolicy string + ExternalCertificate string } // NewCmdCreateReencryptRoute is a macro command to create a reencrypt route. @@ -64,6 +69,7 @@ func NewCmdCreateReencryptRoute(f kcmdutil.Factory, streams genericiooptions.IOS Example: reencryptRouteExample, Run: func(cmd *cobra.Command, args []string) { kcmdutil.CheckErr(o.Complete(f, cmd, args)) + kcmdutil.CheckErr(o.Validate()) kcmdutil.CheckErr(o.Run()) }, } @@ -83,6 +89,7 @@ func NewCmdCreateReencryptRoute(f kcmdutil.Factory, streams genericiooptions.IOS cmd.Flags().StringVar(&o.DestCACert, "dest-ca-cert", o.DestCACert, "Path to a CA certificate file, used for securing the connection from the router to the destination. Defaults to the Service CA.") cmd.MarkFlagFilename("dest-ca-cert") cmd.Flags().StringVar(&o.WildcardPolicy, "wildcard-policy", o.WildcardPolicy, "Sets the WilcardPolicy for the hostname, the default is \"None\". valid values are \"None\" and \"Subdomain\"") + cmd.Flags().StringVar(&o.ExternalCertificate, "external-certificate", o.ExternalCertificate, "Name of a Secret (kubernetes.io/tls) in the same namespace to use as the router certificate. Mutually exclusive with --cert and --key.") kcmdutil.AddValidateFlags(cmd) o.CreateRouteSubcommandOptions.AddFlags(cmd) @@ -95,6 +102,15 @@ func (o *CreateReencryptRouteOptions) Complete(f kcmdutil.Factory, cmd *cobra.Co return o.CreateRouteSubcommandOptions.Complete(f, cmd, args) } +func (o *CreateReencryptRouteOptions) Validate() error { + if len(o.ExternalCertificate) > 0 { + if len(o.Cert) > 0 || len(o.Key) > 0 || len(o.CACert) > 0 { + return fmt.Errorf("--external-certificate is mutually exclusive with --cert, --key, and --ca-cert") + } + } + return nil +} + func (o *CreateReencryptRouteOptions) Run() error { serviceName, err := resolveServiceName(o.CreateRouteSubcommandOptions.Mapper, o.Service) if err != nil { @@ -121,21 +137,25 @@ func (o *CreateReencryptRouteOptions) Run() error { route.Spec.TLS = new(routev1.TLSConfig) route.Spec.TLS.Termination = routev1.TLSTerminationReencrypt - cert, err := fileutil.LoadData(o.Cert) - if err != nil { - return err - } - route.Spec.TLS.Certificate = string(cert) - key, err := fileutil.LoadData(o.Key) - if err != nil { - return err - } - route.Spec.TLS.Key = string(key) - caCert, err := fileutil.LoadData(o.CACert) - if err != nil { - return err + if len(o.ExternalCertificate) > 0 { + route.Spec.TLS.ExternalCertificate = &routev1.LocalObjectReference{Name: o.ExternalCertificate} + } else { + cert, err := fileutil.LoadData(o.Cert) + if err != nil { + return err + } + route.Spec.TLS.Certificate = string(cert) + key, err := fileutil.LoadData(o.Key) + if err != nil { + return err + } + route.Spec.TLS.Key = string(key) + caCert, err := fileutil.LoadData(o.CACert) + if err != nil { + return err + } + route.Spec.TLS.CACertificate = string(caCert) } - route.Spec.TLS.CACertificate = string(caCert) destCACert, err := fileutil.LoadData(o.DestCACert) if err != nil { return err