Skip to content

Commit b32381d

Browse files
committed
Adding option to enable Back End HTTPS for Prow Ingress
Signed-off-by: Nikhil Jiju <[email protected]>
1 parent cfbb719 commit b32381d

File tree

6 files changed

+291
-11
lines changed

6 files changed

+291
-11
lines changed

cmd/deck/main.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ type options struct {
140140
controllerManager prowflagutil.ControllerManagerOptions
141141
dryRun bool
142142
tenantIDs prowflagutil.Strings
143+
enableSSL bool
144+
certFile string
145+
keyFile string
143146
}
144147

145148
func (o *options) Validate() error {
@@ -161,6 +164,15 @@ func (o *options) Validate() error {
161164
if (o.hiddenOnly && o.showHidden) || (o.tenantIDs.Strings() != nil && (o.hiddenOnly || o.showHidden)) {
162165
return errors.New("'--hidden-only', '--tenant-id', and '--show-hidden' are mutually exclusive, 'hidden-only' shows only hidden job, '--tenant-id' shows all jobs with matching ID and 'show-hidden' shows both hidden and non-hidden jobs")
163166
}
167+
168+
if o.enableSSL {
169+
if o.certFile == "" {
170+
return errors.New("flag --enable-ssl was set to true but required flag --cert-file was unset")
171+
}
172+
if o.keyFile == "" {
173+
return errors.New("flag --enable-ssl was set to true but required flag --key-file was unset")
174+
}
175+
}
164176
return nil
165177
}
166178

@@ -186,6 +198,9 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options {
186198
fs.BoolVar(&o.allowInsecure, "allow-insecure", false, "Allows insecure requests for CSRF and GitHub oauth.")
187199
fs.BoolVar(&o.dryRun, "dry-run", false, "Whether or not to make mutating API calls to GitHub.")
188200
fs.Var(&o.tenantIDs, "tenant-id", "The tenantID(s) used by the ProwJobs that should be displayed by this instance of Deck. This flag can be repeated.")
201+
fs.BoolVar(&o.enableSSL, "enable-ssl", false, "Enable SSL to support Ingress backend HTTPS")
202+
fs.StringVar(&o.certFile, "cert-file", "", "Location of the cert file for TLS call. This must be set if SSL is enabled.")
203+
fs.StringVar(&o.keyFile, "key-file", "", "Location of the key file for TLS call. This must be set if SSL is enabled.")
189204
o.config.AddFlags(fs)
190205
o.instrumentation.AddFlags(fs)
191206
o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second
@@ -501,16 +516,27 @@ func main() {
501516
return
502517
}
503518

504-
var server *http.Server
519+
health.ServeReady()
520+
var handler http.Handler
521+
serverPort := ":8080"
522+
505523
if csrfToken != nil {
506524
CSRF := csrf.Protect(csrfToken, csrf.Path("/"), csrf.Secure(!o.allowInsecure))
507-
server = &http.Server{Addr: ":8080", Handler: CSRF(traceHandler(mux))}
525+
handler = CSRF(traceHandler(mux))
508526
} else {
509-
server = &http.Server{Addr: ":8080", Handler: traceHandler(mux)}
527+
handler = traceHandler(mux)
510528
}
511529

512-
health.ServeReady()
513-
interrupts.ListenAndServe(server, 5*time.Second)
530+
if o.enableSSL {
531+
httpServer := &http.Server{
532+
Addr: serverPort,
533+
Handler: handler,
534+
}
535+
interrupts.ListenAndServeTLS(httpServer, o.certFile, o.keyFile, 5*time.Second)
536+
} else {
537+
httpServer := &http.Server{Addr: serverPort, Handler: handler}
538+
interrupts.ListenAndServe(httpServer, 5*time.Second)
539+
}
514540
}
515541

516542
// localOnlyMain contains logic used only when running locally, and is mutually exclusive with
@@ -567,7 +593,7 @@ func prodOnlyMain(cfg config.Getter, pluginAgent *plugins.ConfigAgent, authCfgGe
567593

568594
if o.hookURL != "" {
569595
mux.Handle("/plugin-help.js",
570-
gziphandler.GzipHandler(handlePluginHelp(newHelpAgent(o.hookURL), logrus.WithField("handler", "/plugin-help.js"))))
596+
gziphandler.GzipHandler(handlePluginHelp(newHelpAgent(o.hookURL, o.certFile), logrus.WithField("handler", "/plugin-help.js"))))
571597
}
572598

573599
// tide could potentially be mocked by static data

cmd/deck/main_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,21 @@ func Test_gatherOptions(t *testing.T) {
680680
},
681681
err: true,
682682
},
683+
{
684+
name: "explicitly set enableSSL",
685+
args: map[string]string{
686+
"--enable-ssl": "true",
687+
"--cert-file": "/test/path/cert.pem",
688+
"--key-file": "/test/path/key.pem",
689+
},
690+
expected: func(o *options) {
691+
o.controllerManager.TimeoutListingProwJobs = 30 * time.Second
692+
o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second
693+
o.enableSSL = true
694+
o.certFile = "/test/path/cert.pem"
695+
o.keyFile = "/test/path/key.pem"
696+
},
697+
},
683698
}
684699
for _, tc := range cases {
685700
fs := flag.NewFlagSet("fake-flags", flag.PanicOnError)

cmd/deck/pluginhelp.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"crypto/tls"
21+
"crypto/x509"
2022
"encoding/json"
2123
"fmt"
2224
"net/http"
25+
"net/url"
26+
"os"
2327
"sync"
2428
"time"
2529

@@ -29,18 +33,21 @@ import (
2933
// cacheLife is the time that we keep a pluginhelp.Help struct before considering it stale.
3034
// We consider help valid for a minute to prevent excessive calls to hook.
3135
const cacheLife = time.Minute
36+
const tlsEnabledScehma = "https"
3237

3338
type helpAgent struct {
3439
path string
40+
cert string
3541

3642
sync.Mutex
3743
help *pluginhelp.Help
3844
expiry time.Time
3945
}
4046

41-
func newHelpAgent(path string) *helpAgent {
47+
func newHelpAgent(path string, cert string) *helpAgent {
4248
return &helpAgent{
4349
path: path,
50+
cert: cert,
4451
}
4552
}
4653

@@ -52,6 +59,27 @@ func (ha *helpAgent) getHelp() (*pluginhelp.Help, error) {
5259
return ha.help, nil
5360
}
5461

62+
//Setting Root CAs if SSL is enabled
63+
hookPath, err := url.Parse(ha.path)
64+
if err != nil {
65+
return nil, fmt.Errorf("error parsing hook path: %w", err)
66+
}
67+
if hookPath.Scheme == tlsEnabledScehma {
68+
caCert, err := os.ReadFile(ha.cert)
69+
if err != nil {
70+
return nil, fmt.Errorf("error decoding cert file: %w", err)
71+
}
72+
caCertPool := x509.NewCertPool()
73+
caCertPool.AppendCertsFromPEM(caCert)
74+
//Error is eaten here for testing purposes. There should be no reason the transport is not of type HTTP
75+
if transport, ok := http.DefaultTransport.(*http.Transport); ok {
76+
transport.TLSClientConfig = &tls.Config{
77+
RootCAs: caCertPool,
78+
}
79+
http.DefaultTransport = transport
80+
}
81+
}
82+
5583
var help pluginhelp.Help
5684
resp, err := http.Get(ha.path)
5785
if err != nil {

cmd/deck/pluginhelp_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"bytes"
21+
"encoding/json"
22+
"errors"
23+
"io"
24+
"net/http"
25+
"os"
26+
"sigs.k8s.io/prow/pkg/pluginhelp"
27+
"strings"
28+
"testing"
29+
"time"
30+
)
31+
32+
type mockRoundTripper struct {
33+
resp *http.Response
34+
err error
35+
}
36+
37+
func (m *mockRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
38+
return m.resp, m.err
39+
}
40+
41+
func TestGetHelp(t *testing.T) {
42+
type testCase struct {
43+
name string
44+
hookPath string
45+
cert string
46+
certContent string
47+
mockResp *http.Response
48+
mockErr error
49+
wantDesc string
50+
wantFileErr bool
51+
wantErr string
52+
cacheTest bool
53+
}
54+
55+
pluginHelp := pluginhelp.PluginHelp{Description: "test"}
56+
cached := pluginhelp.PluginHelp{Description: "cached"}
57+
help := pluginhelp.Help{PluginHelp: map[string]pluginhelp.PluginHelp{"example": pluginHelp}}
58+
cachedHelp := pluginhelp.Help{PluginHelp: map[string]pluginhelp.PluginHelp{"example": cached}}
59+
helpBytes, _ := json.Marshal(help)
60+
cachedBytes, _ := json.Marshal(cachedHelp)
61+
62+
cases := []testCase{
63+
{
64+
name: "NoCert_Success",
65+
hookPath: "http://example.com/help",
66+
cert: "",
67+
mockResp: &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(helpBytes))},
68+
wantDesc: "test",
69+
},
70+
{
71+
name: "WithCert_FileError",
72+
hookPath: "https://example.com/help",
73+
cert: "C://badfileformat://",
74+
wantFileErr: true,
75+
wantErr: "error decoding cert file: open C://badfileformat://: no such file or directory",
76+
},
77+
{
78+
name: "NoCert_NoHttps",
79+
hookPath: "http://example.com/help",
80+
cert: "",
81+
mockResp: &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(helpBytes))},
82+
wantDesc: "test",
83+
},
84+
{
85+
name: "WithCert_Success",
86+
hookPath: "https://example.com/help",
87+
cert: t.TempDir() + "/cert.pem",
88+
certContent: "dummy cert content",
89+
mockResp: &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(helpBytes))},
90+
wantDesc: "test",
91+
},
92+
{
93+
name: "HTTPError",
94+
hookPath: "http://example.com/help",
95+
cert: "",
96+
mockErr: errors.New("fail"),
97+
wantErr: "error Getting plugin help: Get \"http://example.com/help\": fail",
98+
},
99+
{
100+
name: "StatusCodeError",
101+
hookPath: "http://example.com/help",
102+
cert: "",
103+
mockResp: &http.Response{StatusCode: 500, Body: io.NopCloser(bytes.NewReader([]byte{}))},
104+
wantErr: "response has status code 500",
105+
},
106+
{
107+
name: "JSONError",
108+
hookPath: "http://example.com/help",
109+
cert: "",
110+
mockResp: &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader([]byte("notjson")))},
111+
wantErr: "error decoding json plugin help:",
112+
},
113+
{
114+
name: "Cache",
115+
hookPath: "http://example.com/help",
116+
cert: "",
117+
mockResp: &http.Response{StatusCode: 200, Body: io.NopCloser(bytes.NewReader(cachedBytes))},
118+
wantDesc: "cached",
119+
cacheTest: true,
120+
},
121+
}
122+
123+
for _, tc := range cases {
124+
t.Run(tc.name, func(t *testing.T) {
125+
126+
//// Mock the HTTP client
127+
http.DefaultTransport = &mockRoundTripper{
128+
resp: tc.mockResp,
129+
err: tc.mockErr,
130+
}
131+
132+
ha := newHelpAgent(tc.hookPath, tc.cert)
133+
if tc.cert != "" && !tc.wantFileErr {
134+
err := os.WriteFile(tc.cert, []byte(tc.certContent), 0644)
135+
if err != nil {
136+
t.Fatalf("Failed to create test file: %v", err)
137+
}
138+
}
139+
140+
if tc.cacheTest {
141+
// First call populates cache
142+
got, err := ha.getHelp()
143+
if err != nil {
144+
t.Fatalf("unexpected error: %v", err)
145+
}
146+
if got.PluginHelp["example"].Description != tc.wantDesc {
147+
t.Errorf("expected description '%s', got '%s'", tc.wantDesc, got.PluginHelp["example"].Description)
148+
}
149+
// Set expiry to future to trigger cache
150+
ha.expiry = time.Now().Add(time.Minute)
151+
got2, err2 := ha.getHelp()
152+
if err2 != nil {
153+
t.Fatalf("unexpected error: %v", err2)
154+
}
155+
if got != got2 {
156+
t.Errorf("expected cached help, got different pointers")
157+
}
158+
return
159+
}
160+
got, err := ha.getHelp()
161+
if tc.wantErr != "" {
162+
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
163+
t.Errorf("expected error containing '%s', got %v", tc.wantErr, err)
164+
}
165+
} else {
166+
if err != nil {
167+
t.Fatalf("unexpected error: %v", err)
168+
}
169+
if got.PluginHelp["example"].Description != tc.wantDesc {
170+
t.Errorf("expected description '%s', got '%s'", tc.wantDesc, got.PluginHelp["example"].Description)
171+
}
172+
}
173+
})
174+
}
175+
}

0 commit comments

Comments
 (0)