Skip to content

Commit e0ac307

Browse files
committed
feat: implement '-external-certificate' flag to edge and reencrypt route
Adds support for referencing TLS certificates from a TLS Secret when creating edge and reencrypt routes. The new '--external-certificate' flag accepts a Secret name and populates 'route.Spec.TLS.ExternalCertificate' as a LocalObjectReference. This flag is mutually exclusive with '--cert' and '--key', since inline certificates and secret-backed certificates are alternative sources. The '--ca-cert' flag (and '--dest-ca-cert' for reencrypt routes) remains compatible with '--external-certificate'.
1 parent 66dee73 commit e0ac307

4 files changed

Lines changed: 395 additions & 37 deletions

File tree

pkg/cli/create/routeedge.go

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,25 @@ var (
3434
# Create an edge route that exposes the frontend service and specify a path
3535
# If the route name is omitted, the service name will be used
3636
oc create route edge --service=frontend --path /assets
37+
38+
# Create an edge route that uses an external certificate from a secret
39+
oc create route edge --service=frontend --external-certificate=my-cert-secret
3740
`)
3841
)
3942

4043
type CreateEdgeRouteOptions struct {
4144
CreateRouteSubcommandOptions *CreateRouteSubcommandOptions
4245

43-
Hostname string
44-
Port string
45-
InsecurePolicy string
46-
Service string
47-
Path string
48-
Cert string
49-
Key string
50-
CACert string
51-
WildcardPolicy string
46+
Hostname string
47+
Port string
48+
InsecurePolicy string
49+
Service string
50+
Path string
51+
Cert string
52+
Key string
53+
CACert string
54+
ExternalCertificate string
55+
WildcardPolicy string
5256
}
5357

5458
// NewCmdCreateEdgeRoute is a macro command to create an edge route.
@@ -63,6 +67,7 @@ func NewCmdCreateEdgeRoute(f kcmdutil.Factory, streams genericiooptions.IOStream
6367
Example: edgeRouteExample,
6468
Run: func(cmd *cobra.Command, args []string) {
6569
kcmdutil.CheckErr(o.Complete(f, cmd, args))
70+
kcmdutil.CheckErr(o.Validate())
6671
kcmdutil.CheckErr(o.Run())
6772
},
6873
}
@@ -79,6 +84,7 @@ func NewCmdCreateEdgeRoute(f kcmdutil.Factory, streams genericiooptions.IOStream
7984
cmd.MarkFlagFilename("key")
8085
cmd.Flags().StringVar(&o.CACert, "ca-cert", o.CACert, "Path to a CA certificate file.")
8186
cmd.MarkFlagFilename("ca-cert")
87+
cmd.Flags().StringVar(&o.ExternalCertificate, "external-certificate", o.ExternalCertificate, "Name of a secret that contains the TLS certificate and key. The secret must contain keys named tls.crt and tls.key. Mutually exclusive with --cert and --key.")
8288
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\"")
8389

8490
kcmdutil.AddValidateFlags(cmd)
@@ -92,6 +98,16 @@ func (o *CreateEdgeRouteOptions) Complete(f kcmdutil.Factory, cmd *cobra.Command
9298
return o.CreateRouteSubcommandOptions.Complete(f, cmd, args)
9399
}
94100

101+
func (o *CreateEdgeRouteOptions) Validate() error {
102+
if len(o.Cert) > 0 && len(o.ExternalCertificate) > 0 {
103+
return fmt.Errorf("--cert and --external-certificate are mutually exclusive")
104+
}
105+
if len(o.Key) > 0 && len(o.ExternalCertificate) > 0 {
106+
return fmt.Errorf("--key and --external-certificate are mutually exclusive")
107+
}
108+
return nil
109+
}
110+
95111
func (o *CreateEdgeRouteOptions) Run() error {
96112
serviceName, err := resolveServiceName(o.CreateRouteSubcommandOptions.Mapper, o.Service)
97113
if err != nil {
@@ -111,16 +127,24 @@ func (o *CreateEdgeRouteOptions) Run() error {
111127

112128
route.Spec.TLS = new(routev1.TLSConfig)
113129
route.Spec.TLS.Termination = routev1.TLSTerminationEdge
114-
cert, err := fileutil.LoadData(o.Cert)
115-
if err != nil {
116-
return err
117-
}
118-
route.Spec.TLS.Certificate = string(cert)
119-
key, err := fileutil.LoadData(o.Key)
120-
if err != nil {
121-
return err
130+
131+
if len(o.ExternalCertificate) > 0 {
132+
route.Spec.TLS.ExternalCertificate = &routev1.LocalObjectReference{
133+
Name: o.ExternalCertificate,
134+
}
135+
} else {
136+
cert, err := fileutil.LoadData(o.Cert)
137+
if err != nil {
138+
return err
139+
}
140+
route.Spec.TLS.Certificate = string(cert)
141+
key, err := fileutil.LoadData(o.Key)
142+
if err != nil {
143+
return err
144+
}
145+
route.Spec.TLS.Key = string(key)
122146
}
123-
route.Spec.TLS.Key = string(key)
147+
124148
caCert, err := fileutil.LoadData(o.CACert)
125149
if err != nil {
126150
return err

pkg/cli/create/routeedge_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package create
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"os"
7+
"strings"
8+
"testing"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
"k8s.io/apimachinery/pkg/api/meta"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/util/intstr"
14+
"k8s.io/cli-runtime/pkg/genericclioptions"
15+
"k8s.io/cli-runtime/pkg/genericiooptions"
16+
fakekubernetes "k8s.io/client-go/kubernetes/fake"
17+
routefake "github.com/openshift/client-go/route/clientset/versioned/fake"
18+
)
19+
20+
func writeTestFile(t *testing.T, path, content string) {
21+
t.Helper()
22+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
23+
t.Fatalf("failed to write test file %s: %v", path, err)
24+
}
25+
}
26+
27+
func newTestService() *corev1.Service {
28+
return &corev1.Service{
29+
ObjectMeta: metav1.ObjectMeta{
30+
Name: "my-service",
31+
Namespace: "default",
32+
},
33+
Spec: corev1.ServiceSpec{
34+
Ports: []corev1.ServicePort{
35+
{
36+
Name: "http",
37+
Port: 80,
38+
TargetPort: intstr.FromInt32(8080),
39+
Protocol: corev1.ProtocolTCP,
40+
},
41+
},
42+
},
43+
}
44+
}
45+
46+
func newTestRouteSubcommandOptions(t *testing.T) (*CreateRouteSubcommandOptions, *routefake.Clientset, *bytes.Buffer) {
47+
t.Helper()
48+
service := newTestService()
49+
streams, _, out, _ := genericiooptions.NewTestIOStreams()
50+
fakeKubeClient := fakekubernetes.NewClientset(service)
51+
fakeRouteClientset := routefake.NewClientset()
52+
mapper := meta.NewDefaultRESTMapper(nil)
53+
54+
printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(createCmdScheme)
55+
printer, err := printFlags.ToPrinter()
56+
if err != nil {
57+
t.Fatalf("failed to create printer: %v", err)
58+
}
59+
60+
sub := &CreateRouteSubcommandOptions{
61+
PrintFlags: printFlags,
62+
Name: "my-route",
63+
Namespace: "default",
64+
Mapper: mapper,
65+
Client: fakeRouteClientset.RouteV1(),
66+
CoreClient: fakeKubeClient.CoreV1(),
67+
Printer: printer,
68+
IOStreams: streams,
69+
}
70+
71+
return sub, fakeRouteClientset, out
72+
}
73+
74+
func newTestEdgeRouteOptions(t *testing.T) (*CreateEdgeRouteOptions, *routefake.Clientset, *bytes.Buffer) {
75+
t.Helper()
76+
sub, fakeRouteClientset, out := newTestRouteSubcommandOptions(t)
77+
o := &CreateEdgeRouteOptions{
78+
CreateRouteSubcommandOptions: sub,
79+
Service: "my-service",
80+
}
81+
return o, fakeRouteClientset, out
82+
}
83+
84+
func TestCreateEdgeRoute_MutualExclusivity(t *testing.T) {
85+
tmpDir := t.TempDir()
86+
certFile := tmpDir + "/tls.crt"
87+
keyFile := tmpDir + "/tls.key"
88+
writeTestFile(t, certFile, "test-cert-data")
89+
writeTestFile(t, keyFile, "test-key-data")
90+
91+
tests := []struct {
92+
name string
93+
cert string
94+
key string
95+
externalCertificate string
96+
expectError string
97+
}{
98+
{
99+
name: "neither --cert nor --external-certificate set",
100+
},
101+
{
102+
name: "only --cert set",
103+
cert: certFile,
104+
},
105+
{
106+
name: "only --external-certificate set",
107+
externalCertificate: "my-secret",
108+
},
109+
{
110+
name: "both --cert and --external-certificate set",
111+
cert: certFile,
112+
externalCertificate: "my-secret",
113+
expectError: "--cert and --external-certificate are mutually exclusive",
114+
},
115+
{
116+
name: "both --key and --external-certificate set",
117+
key: keyFile,
118+
externalCertificate: "my-secret",
119+
expectError: "--key and --external-certificate are mutually exclusive",
120+
},
121+
}
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
o, _, _ := newTestEdgeRouteOptions(t)
125+
o.Cert = tt.cert
126+
o.Key = tt.key
127+
o.ExternalCertificate = tt.externalCertificate
128+
129+
err := o.Validate()
130+
if tt.expectError != "" {
131+
if err == nil {
132+
t.Fatal("expected error but got nil")
133+
}
134+
if !strings.Contains(err.Error(), tt.expectError) {
135+
t.Fatalf("expected error containing %q, got: %v", tt.expectError, err)
136+
}
137+
return
138+
}
139+
if err != nil {
140+
t.Fatalf("unexpected error: %v", err)
141+
}
142+
})
143+
}
144+
}
145+
146+
func TestCreateEdgeRoute_ExternalCertificateWiring(t *testing.T) {
147+
o, fakeRouteClientset, _ := newTestEdgeRouteOptions(t)
148+
o.ExternalCertificate = "my-cert-secret"
149+
150+
if err := o.Run(); err != nil {
151+
t.Fatalf("unexpected error: %v", err)
152+
}
153+
154+
routes, err := fakeRouteClientset.RouteV1().Routes("default").List(context.TODO(), metav1.ListOptions{})
155+
if err != nil {
156+
t.Fatalf("failed to list routes: %v", err)
157+
}
158+
if len(routes.Items) != 1 {
159+
t.Fatalf("expected 1 route, got %d", len(routes.Items))
160+
}
161+
162+
route := routes.Items[0]
163+
if route.Name != "my-route" {
164+
t.Errorf("expected route name %q, got %q", "my-route", route.Name)
165+
}
166+
if route.Spec.TLS == nil {
167+
t.Fatal("expected TLS config to be set")
168+
}
169+
if route.Spec.TLS.ExternalCertificate == nil {
170+
t.Fatal("expected ExternalCertificate to be set")
171+
}
172+
if route.Spec.TLS.ExternalCertificate.Name != "my-cert-secret" {
173+
t.Errorf("expected ExternalCertificate.Name %q, got %q", "my-cert-secret", route.Spec.TLS.ExternalCertificate.Name)
174+
}
175+
if route.Spec.TLS.Certificate != "" {
176+
t.Errorf("expected no inline certificate, got %q", route.Spec.TLS.Certificate)
177+
}
178+
if route.Spec.TLS.Key != "" {
179+
t.Errorf("expected no inline key, got %q", route.Spec.TLS.Key)
180+
}
181+
if route.Spec.TLS.Termination != "edge" {
182+
t.Errorf("expected termination %q, got %q", "edge", route.Spec.TLS.Termination)
183+
}
184+
}
185+

pkg/cli/create/routereenecrypt.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package create
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/spf13/cobra"
78

@@ -34,22 +35,26 @@ var (
3435
# route name default to the service name and the destination CA certificate
3536
# default to the service CA
3637
oc create route reencrypt --service=frontend
38+
39+
# Create a reencrypt route that uses an external certificate from a secret
40+
oc create route reencrypt --service=frontend --external-certificate=my-cert-secret --dest-ca-cert cert.cert
3741
`)
3842
)
3943

4044
type CreateReencryptRouteOptions struct {
4145
CreateRouteSubcommandOptions *CreateRouteSubcommandOptions
4246

43-
Hostname string
44-
Port string
45-
InsecurePolicy string
46-
Service string
47-
Path string
48-
Cert string
49-
Key string
50-
CACert string
51-
DestCACert string
52-
WildcardPolicy string
47+
Hostname string
48+
Port string
49+
InsecurePolicy string
50+
Service string
51+
Path string
52+
Cert string
53+
Key string
54+
CACert string
55+
DestCACert string
56+
ExternalCertificate string
57+
WildcardPolicy string
5358
}
5459

5560
// NewCmdCreateReencryptRoute is a macro command to create a reencrypt route.
@@ -64,6 +69,7 @@ func NewCmdCreateReencryptRoute(f kcmdutil.Factory, streams genericiooptions.IOS
6469
Example: reencryptRouteExample,
6570
Run: func(cmd *cobra.Command, args []string) {
6671
kcmdutil.CheckErr(o.Complete(f, cmd, args))
72+
kcmdutil.CheckErr(o.Validate())
6773
kcmdutil.CheckErr(o.Run())
6874
},
6975
}
@@ -82,6 +88,7 @@ func NewCmdCreateReencryptRoute(f kcmdutil.Factory, streams genericiooptions.IOS
8288
cmd.MarkFlagFilename("ca-cert")
8389
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.")
8490
cmd.MarkFlagFilename("dest-ca-cert")
91+
cmd.Flags().StringVar(&o.ExternalCertificate, "external-certificate", o.ExternalCertificate, "Name of a secret that contains the TLS certificate and key. The secret must contain keys named tls.crt and tls.key. Mutually exclusive with --cert and --key.")
8592
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\"")
8693

8794
kcmdutil.AddValidateFlags(cmd)
@@ -95,6 +102,16 @@ func (o *CreateReencryptRouteOptions) Complete(f kcmdutil.Factory, cmd *cobra.Co
95102
return o.CreateRouteSubcommandOptions.Complete(f, cmd, args)
96103
}
97104

105+
func (o *CreateReencryptRouteOptions) Validate() error {
106+
if len(o.Cert) > 0 && len(o.ExternalCertificate) > 0 {
107+
return fmt.Errorf("--cert and --external-certificate are mutually exclusive")
108+
}
109+
if len(o.Key) > 0 && len(o.ExternalCertificate) > 0 {
110+
return fmt.Errorf("--key and --external-certificate are mutually exclusive")
111+
}
112+
return nil
113+
}
114+
98115
func (o *CreateReencryptRouteOptions) Run() error {
99116
serviceName, err := resolveServiceName(o.CreateRouteSubcommandOptions.Mapper, o.Service)
100117
if err != nil {
@@ -121,16 +138,23 @@ func (o *CreateReencryptRouteOptions) Run() error {
121138
route.Spec.TLS = new(routev1.TLSConfig)
122139
route.Spec.TLS.Termination = routev1.TLSTerminationReencrypt
123140

124-
cert, err := fileutil.LoadData(o.Cert)
125-
if err != nil {
126-
return err
127-
}
128-
route.Spec.TLS.Certificate = string(cert)
129-
key, err := fileutil.LoadData(o.Key)
130-
if err != nil {
131-
return err
141+
if len(o.ExternalCertificate) > 0 {
142+
route.Spec.TLS.ExternalCertificate = &routev1.LocalObjectReference{
143+
Name: o.ExternalCertificate,
144+
}
145+
} else {
146+
cert, err := fileutil.LoadData(o.Cert)
147+
if err != nil {
148+
return err
149+
}
150+
route.Spec.TLS.Certificate = string(cert)
151+
key, err := fileutil.LoadData(o.Key)
152+
if err != nil {
153+
return err
154+
}
155+
route.Spec.TLS.Key = string(key)
132156
}
133-
route.Spec.TLS.Key = string(key)
157+
134158
caCert, err := fileutil.LoadData(o.CACert)
135159
if err != nil {
136160
return err

0 commit comments

Comments
 (0)