Skip to content

Commit 9225a5f

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 9225a5f

4 files changed

Lines changed: 584 additions & 19 deletions

File tree

pkg/cli/create/routeedge.go

Lines changed: 27 additions & 9 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.
@@ -79,6 +83,7 @@ func NewCmdCreateEdgeRoute(f kcmdutil.Factory, streams genericiooptions.IOStream
7983
cmd.MarkFlagFilename("key")
8084
cmd.Flags().StringVar(&o.CACert, "ca-cert", o.CACert, "Path to a CA certificate file.")
8185
cmd.MarkFlagFilename("ca-cert")
86+
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.")
8287
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\"")
8388

8489
kcmdutil.AddValidateFlags(cmd)
@@ -93,6 +98,13 @@ func (o *CreateEdgeRouteOptions) Complete(f kcmdutil.Factory, cmd *cobra.Command
9398
}
9499

95100
func (o *CreateEdgeRouteOptions) Run() error {
101+
if len(o.Cert) > 0 && len(o.ExternalCertificate) > 0 {
102+
return fmt.Errorf("--cert and --external-certificate are mutually exclusive")
103+
}
104+
if len(o.Key) > 0 && len(o.ExternalCertificate) > 0 {
105+
return fmt.Errorf("--key and --external-certificate are mutually exclusive")
106+
}
107+
96108
serviceName, err := resolveServiceName(o.CreateRouteSubcommandOptions.Mapper, o.Service)
97109
if err != nil {
98110
return err
@@ -127,6 +139,12 @@ func (o *CreateEdgeRouteOptions) Run() error {
127139
}
128140
route.Spec.TLS.CACertificate = string(caCert)
129141

142+
if len(o.ExternalCertificate) > 0 {
143+
route.Spec.TLS.ExternalCertificate = &routev1.LocalObjectReference{
144+
Name: o.ExternalCertificate,
145+
}
146+
}
147+
130148
if len(o.InsecurePolicy) > 0 {
131149
route.Spec.TLS.InsecureEdgeTerminationPolicy = routev1.InsecureEdgeTerminationPolicyType(o.InsecurePolicy)
132150
}

pkg/cli/create/routeedge_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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+
kcmdutil "k8s.io/kubectl/pkg/cmd/util"
18+
19+
routefake "github.com/openshift/client-go/route/clientset/versioned/fake"
20+
)
21+
22+
func writeTestFile(t *testing.T, path, content string) {
23+
t.Helper()
24+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
25+
t.Fatalf("failed to write test file %s: %v", path, err)
26+
}
27+
}
28+
29+
func newTestService() *corev1.Service {
30+
return &corev1.Service{
31+
ObjectMeta: metav1.ObjectMeta{
32+
Name: "my-service",
33+
Namespace: "default",
34+
},
35+
Spec: corev1.ServiceSpec{
36+
Ports: []corev1.ServicePort{
37+
{
38+
Name: "http",
39+
Port: 80,
40+
TargetPort: intstr.FromInt32(8080),
41+
Protocol: corev1.ProtocolTCP,
42+
},
43+
},
44+
},
45+
}
46+
}
47+
48+
func newTestEdgeRouteOptions(t *testing.T) (*CreateEdgeRouteOptions, *routefake.Clientset, *bytes.Buffer) {
49+
t.Helper()
50+
service := newTestService()
51+
streams, _, out, _ := genericiooptions.NewTestIOStreams()
52+
fakeKubeClient := fakekubernetes.NewClientset(service)
53+
fakeRouteClientset := routefake.NewClientset()
54+
mapper := meta.NewDefaultRESTMapper(nil)
55+
56+
printFlags := genericclioptions.NewPrintFlags("created").WithTypeSetter(createCmdScheme)
57+
printer, err := printFlags.ToPrinter()
58+
if err != nil {
59+
t.Fatalf("failed to create printer: %v", err)
60+
}
61+
62+
o := &CreateEdgeRouteOptions{
63+
CreateRouteSubcommandOptions: &CreateRouteSubcommandOptions{
64+
PrintFlags: printFlags,
65+
Name: "my-route",
66+
Namespace: "default",
67+
Mapper: mapper,
68+
Client: fakeRouteClientset.RouteV1(),
69+
CoreClient: fakeKubeClient.CoreV1(),
70+
Printer: printer,
71+
IOStreams: streams,
72+
},
73+
Service: "my-service",
74+
}
75+
76+
return o, fakeRouteClientset, out
77+
}
78+
79+
func TestCreateEdgeRoute_MutualExclusivity(t *testing.T) {
80+
tmpDir := t.TempDir()
81+
certFile := tmpDir + "/tls.crt"
82+
keyFile := tmpDir + "/tls.key"
83+
writeTestFile(t, certFile, "test-cert-data")
84+
writeTestFile(t, keyFile, "test-key-data")
85+
86+
tests := []struct {
87+
name string
88+
cert string
89+
key string
90+
externalCertificate string
91+
expectError string
92+
}{
93+
{
94+
name: "neither --cert nor --external-certificate set",
95+
},
96+
{
97+
name: "only --cert set",
98+
cert: certFile,
99+
},
100+
{
101+
name: "only --external-certificate set",
102+
externalCertificate: "my-secret",
103+
},
104+
{
105+
name: "both --cert and --external-certificate set",
106+
cert: certFile,
107+
externalCertificate: "my-secret",
108+
expectError: "--cert and --external-certificate are mutually exclusive",
109+
},
110+
{
111+
name: "both --key and --external-certificate set",
112+
key: keyFile,
113+
externalCertificate: "my-secret",
114+
expectError: "--key and --external-certificate are mutually exclusive",
115+
},
116+
}
117+
for _, tt := range tests {
118+
t.Run(tt.name, func(t *testing.T) {
119+
o, _, _ := newTestEdgeRouteOptions(t)
120+
o.Cert = tt.cert
121+
o.Key = tt.key
122+
o.ExternalCertificate = tt.externalCertificate
123+
124+
err := o.Run()
125+
if tt.expectError != "" {
126+
if err == nil {
127+
t.Fatal("expected error but got nil")
128+
}
129+
if !strings.Contains(err.Error(), tt.expectError) {
130+
t.Fatalf("expected error containing %q, got: %v", tt.expectError, err)
131+
}
132+
return
133+
}
134+
if err != nil {
135+
t.Fatalf("unexpected error: %v", err)
136+
}
137+
})
138+
}
139+
}
140+
141+
func TestCreateEdgeRoute_ExternalCertificateWiring(t *testing.T) {
142+
o, fakeRouteClientset, _ := newTestEdgeRouteOptions(t)
143+
o.ExternalCertificate = "my-cert-secret"
144+
145+
if err := o.Run(); err != nil {
146+
t.Fatalf("unexpected error: %v", err)
147+
}
148+
149+
routes, err := fakeRouteClientset.RouteV1().Routes("default").List(context.TODO(), metav1.ListOptions{})
150+
if err != nil {
151+
t.Fatalf("failed to list routes: %v", err)
152+
}
153+
if len(routes.Items) != 1 {
154+
t.Fatalf("expected 1 route, got %d", len(routes.Items))
155+
}
156+
157+
route := routes.Items[0]
158+
if route.Name != "my-route" {
159+
t.Errorf("expected route name %q, got %q", "my-route", route.Name)
160+
}
161+
if route.Spec.TLS == nil {
162+
t.Fatal("expected TLS config to be set")
163+
}
164+
if route.Spec.TLS.ExternalCertificate == nil {
165+
t.Fatal("expected ExternalCertificate to be set")
166+
}
167+
if route.Spec.TLS.ExternalCertificate.Name != "my-cert-secret" {
168+
t.Errorf("expected ExternalCertificate.Name %q, got %q", "my-cert-secret", route.Spec.TLS.ExternalCertificate.Name)
169+
}
170+
if route.Spec.TLS.Certificate != "" {
171+
t.Errorf("expected no inline certificate, got %q", route.Spec.TLS.Certificate)
172+
}
173+
if route.Spec.TLS.Termination != "edge" {
174+
t.Errorf("expected termination %q, got %q", "edge", route.Spec.TLS.Termination)
175+
}
176+
}
177+
178+
func TestCreateEdgeRoute_ExternalCertificateWithCACert(t *testing.T) {
179+
o, fakeRouteClientset, _ := newTestEdgeRouteOptions(t)
180+
o.ExternalCertificate = "my-cert-secret"
181+
182+
tmpDir := t.TempDir()
183+
caCertFile := tmpDir + "/ca.crt"
184+
writeTestFile(t, caCertFile, "test-ca-cert-data")
185+
o.CACert = caCertFile
186+
187+
if err := o.Run(); err != nil {
188+
t.Fatalf("unexpected error: %v", err)
189+
}
190+
191+
routes, err := fakeRouteClientset.RouteV1().Routes("default").List(context.TODO(), metav1.ListOptions{})
192+
if err != nil {
193+
t.Fatalf("failed to list routes: %v", err)
194+
}
195+
if len(routes.Items) != 1 {
196+
t.Fatalf("expected 1 route, got %d", len(routes.Items))
197+
}
198+
199+
route := routes.Items[0]
200+
if route.Spec.TLS.ExternalCertificate == nil {
201+
t.Fatal("expected ExternalCertificate to be set")
202+
}
203+
if route.Spec.TLS.ExternalCertificate.Name != "my-cert-secret" {
204+
t.Errorf("expected ExternalCertificate.Name %q, got %q", "my-cert-secret", route.Spec.TLS.ExternalCertificate.Name)
205+
}
206+
if route.Spec.TLS.CACertificate != "test-ca-cert-data" {
207+
t.Errorf("expected CACertificate %q, got %q", "test-ca-cert-data", route.Spec.TLS.CACertificate)
208+
}
209+
}
210+
211+
func TestCreateEdgeRoute_NoExternalCertificate(t *testing.T) {
212+
o, fakeRouteClientset, _ := newTestEdgeRouteOptions(t)
213+
214+
if err := o.Run(); err != nil {
215+
t.Fatalf("unexpected error: %v", err)
216+
}
217+
218+
routes, err := fakeRouteClientset.RouteV1().Routes("default").List(context.TODO(), metav1.ListOptions{})
219+
if err != nil {
220+
t.Fatalf("failed to list routes: %v", err)
221+
}
222+
if len(routes.Items) != 1 {
223+
t.Fatalf("expected 1 route, got %d", len(routes.Items))
224+
}
225+
226+
route := routes.Items[0]
227+
if route.Spec.TLS == nil {
228+
t.Fatal("expected TLS config to be set")
229+
}
230+
if route.Spec.TLS.ExternalCertificate != nil {
231+
t.Errorf("expected no ExternalCertificate, got %+v", route.Spec.TLS.ExternalCertificate)
232+
}
233+
}
234+
235+
func TestCreateEdgeRoute_DryRunYAMLOutput(t *testing.T) {
236+
o, _, out := newTestEdgeRouteOptions(t)
237+
o.ExternalCertificate = "my-cert-secret"
238+
o.CreateRouteSubcommandOptions.DryRunStrategy = kcmdutil.DryRunClient
239+
240+
yamlFormat := "yaml"
241+
o.CreateRouteSubcommandOptions.PrintFlags.OutputFormat = &yamlFormat
242+
printer, err := o.CreateRouteSubcommandOptions.PrintFlags.ToPrinter()
243+
if err != nil {
244+
t.Fatalf("failed to create yaml printer: %v", err)
245+
}
246+
o.CreateRouteSubcommandOptions.Printer = printer
247+
248+
if err := o.Run(); err != nil {
249+
t.Fatalf("unexpected error: %v", err)
250+
}
251+
252+
output := out.String()
253+
if !strings.Contains(output, "externalCertificate") {
254+
t.Errorf("expected YAML output to contain 'externalCertificate', got:\n%s", output)
255+
}
256+
if !strings.Contains(output, "my-cert-secret") {
257+
t.Errorf("expected YAML output to contain 'my-cert-secret', got:\n%s", output)
258+
}
259+
if !strings.Contains(output, "termination: edge") {
260+
t.Errorf("expected YAML output to contain 'termination: edge', got:\n%s", output)
261+
}
262+
}

0 commit comments

Comments
 (0)