-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
spoof.go
183 lines (151 loc) · 5.41 KB
/
spoof.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
Copyright 2018 The Knative Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// spoof contains logic to make polling HTTP requests against an endpoint with optional host spoofing.
package spoof
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
"go.uber.org/zap"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
)
const (
requestInterval = 1 * time.Second
requestTimeout = 5 * time.Minute
)
// Response is a stripped down subset of http.Response. The is primarily useful
// for ResponseCheckers to inspect the response body without consuming it.
// Notably, Body is a byte slice instead of an io.ReadCloser.
type Response struct {
Status string
StatusCode int
Header http.Header
Body []byte
}
type Interface interface {
Do(*http.Request) (*Response, error)
Poll(*http.Request, ResponseChecker) (*Response, error)
}
// https://medium.com/stupid-gopher-tricks/ensuring-go-interface-satisfaction-at-compile-time-1ed158e8fa17
var _ Interface = (*SpoofingClient)(nil)
// ResponseChecker is used to determine when SpoofinClient.Poll is done polling.
// This allows you to predicate wait.PollImmediate on the request's http.Response.
//
// See the apimachinery wait package:
// https://github.com/kubernetes/apimachinery/blob/cf7ae2f57dabc02a3d215f15ca61ae1446f3be8f/pkg/util/wait/wait.go#L172
type ResponseChecker func(resp *Response) (done bool, err error)
// SpoofingClient is a minimal http client wrapper that spoofs the domain of requests
// for non-resolvable domains.
type SpoofingClient struct {
Client *http.Client
RequestInterval time.Duration
RequestTimeout time.Duration
RetryCodes []int
endpoint string
domain string
logger *zap.SugaredLogger
}
// New returns a SpoofingClient that rewrites requests if the target domain is not `resolveable`.
// It does this by looking up the ingress at construction time, so reusing a client will not
// follow the ingress if it moves (or if there are multiple ingresses).
//
// If that's a problem, see test/request.go#WaitForEndpointState for oneshot spoofing.
func New(kubeClientset *kubernetes.Clientset, logger *zap.SugaredLogger, domain string, resolvable bool) (*SpoofingClient, error) {
sc := SpoofingClient{
Client: http.DefaultClient,
RequestInterval: requestInterval,
RequestTimeout: requestTimeout,
logger: logger,
}
if !resolvable {
// If the domain that the Route controller is configured to assign to Route.Status.Domain
// (the domainSuffix) is not resolvable, we need to retrieve the IP of the endpoint and
// spoof the Host in our requests.
// TODO(tcnghia): These probably shouldn't be hard-coded here?
ingressName := "knative-ingressgateway"
ingressNamespace := "istio-system"
ingress, err := kubeClientset.CoreV1().Services(ingressNamespace).Get(ingressName, metav1.GetOptions{})
if err != nil {
return nil, err
}
if ingress.Status.LoadBalancer.Ingress[0].IP == "" {
return nil, fmt.Errorf("Expected ingress loadbalancer IP for %s to be set, instead was empty", ingressName)
}
sc.endpoint = ingress.Status.LoadBalancer.Ingress[0].IP
sc.domain = domain
} else {
// If the domain is resolvable, we can use it directly when we make requests.
sc.endpoint = domain
}
return &sc, nil
}
// Do dispatches to the underlying http.Client.Do, spoofing domains as needed
// and transforming the http.Response into a spoof.Response.
func (sc *SpoofingClient) Do(req *http.Request) (*Response, error) {
// Controls the Host header, for spoofing.
if sc.domain != "" {
req.Host = sc.domain
}
// Controls the actual resolution.
if sc.endpoint != "" {
req.URL.Host = sc.endpoint
}
resp, err := sc.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return &Response{
Status: resp.Status,
StatusCode: resp.StatusCode,
Header: resp.Header,
Body: body,
}, nil
}
// Poll executes an http request until it satisfies the inState condition or encounters an error.
func (sc *SpoofingClient) Poll(req *http.Request, inState ResponseChecker) (*Response, error) {
var (
resp *Response
err error
)
err = wait.PollImmediate(sc.RequestInterval, sc.RequestTimeout, func() (bool, error) {
resp, err = sc.Do(req)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
sc.logger.Infof("Retrying for TCP timeout %v", err)
return false, nil
}
return true, err
}
// TODO(jonjohnson): This could just be pulled out into a retrying ResponseChecker middleware thing.
if resp.StatusCode != http.StatusOK {
for _, code := range sc.RetryCodes {
if resp.StatusCode == code {
sc.logger.Infof("Retrying for code %v", resp.StatusCode)
return false, nil
}
}
return true, fmt.Errorf("Status code %d was not a retriable code (%v)", resp.StatusCode, sc.RetryCodes)
}
return inState(resp)
})
return resp, err
}