mirror of https://github.com/knative/caching.git
Auto-update dependencies (#93)
Produced via: `dep ensure -update knative.dev/test-infra knative.dev/pkg` /assign mattmoor
This commit is contained in:
parent
effd5c6430
commit
e3145bc1cb
|
@ -927,7 +927,7 @@
|
|||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:80ebfb8d1bf38f2d1e3bf52eaa074a62e04c6b506ea5f00dc39596425e00ee5a"
|
||||
digest = "1:c58bb491fe5be973899a0c744db8a283745641503bed8ddb683da4ad9bff09c5"
|
||||
name = "knative.dev/pkg"
|
||||
packages = [
|
||||
"apis",
|
||||
|
@ -946,7 +946,7 @@
|
|||
"metrics/metricskey",
|
||||
]
|
||||
pruneopts = "T"
|
||||
revision = "e9c7aefce07abe1e9b7246a2f73537a68726f244"
|
||||
revision = "75da3911c0205df6c1b8acb74662d96a6e37b305"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
@ -957,7 +957,7 @@
|
|||
"tools/dep-collector",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "305dd68b036713ee60a38caab1d8531a20e741d1"
|
||||
revision = "cfee08011fea83b3a3b4fc0d3dfe436789a1ac24"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
|
|
|
@ -38,8 +38,8 @@ type KubeClient struct {
|
|||
}
|
||||
|
||||
// NewSpoofingClient returns a spoofing client to make requests
|
||||
func NewSpoofingClient(client *KubeClient, logf logging.FormatLogger, domain string, resolvable bool) (*spoof.SpoofingClient, error) {
|
||||
return spoof.New(client.Kube, logf, domain, resolvable, Flags.IngressEndpoint)
|
||||
func NewSpoofingClient(client *KubeClient, logf logging.FormatLogger, domain string, resolvable bool, opts ...spoof.TransportOption) (*spoof.SpoofingClient, error) {
|
||||
return spoof.New(client.Kube, logf, domain, resolvable, Flags.IngressEndpoint, opts...)
|
||||
}
|
||||
|
||||
// NewKubeClient instantiates and returns several clientsets required for making request to the
|
||||
|
|
|
@ -137,11 +137,17 @@ func (k *kubelogs) handleLine(l string) {
|
|||
continue
|
||||
}
|
||||
|
||||
// We also get logs not from controllers (activator, autoscaler).
|
||||
// So replace controller string in them with their callsite.
|
||||
site := line.Controller
|
||||
if site == "" {
|
||||
site = line.Caller
|
||||
}
|
||||
// E 15:04:05.000 [route-controller] [default/testroute-xyz] this is my message
|
||||
msg := fmt.Sprintf("%s %s [%s] [%s] %s",
|
||||
strings.ToUpper(string(line.Level[0])),
|
||||
line.Timestamp.Format(timeFormat),
|
||||
line.Controller,
|
||||
site,
|
||||
line.Key,
|
||||
line.Message)
|
||||
|
||||
|
|
|
@ -134,7 +134,14 @@ func MatchesAllOf(checkers ...spoof.ResponseChecker) spoof.ResponseChecker {
|
|||
// the domain in the request headers, otherwise it will make the request directly to domain.
|
||||
// desc will be used to name the metric that is emitted to track how long it took for the
|
||||
// domain to get into the state checked by inState. Commas in `desc` must be escaped.
|
||||
func WaitForEndpointState(kubeClient *KubeClient, logf logging.FormatLogger, theURL string, inState spoof.ResponseChecker, desc string, resolvable bool, opts ...RequestOption) (*spoof.Response, error) {
|
||||
func WaitForEndpointState(
|
||||
kubeClient *KubeClient,
|
||||
logf logging.FormatLogger,
|
||||
theURL string,
|
||||
inState spoof.ResponseChecker,
|
||||
desc string,
|
||||
resolvable bool,
|
||||
opts ...interface{}) (*spoof.Response, error) {
|
||||
return WaitForEndpointStateWithTimeout(kubeClient, logf, theURL, inState, desc, resolvable, spoof.RequestTimeout, opts...)
|
||||
}
|
||||
|
||||
|
@ -145,8 +152,14 @@ func WaitForEndpointState(kubeClient *KubeClient, logf logging.FormatLogger, the
|
|||
// desc will be used to name the metric that is emitted to track how long it took for the
|
||||
// domain to get into the state checked by inState. Commas in `desc` must be escaped.
|
||||
func WaitForEndpointStateWithTimeout(
|
||||
kubeClient *KubeClient, logf logging.FormatLogger, theURL string, inState spoof.ResponseChecker,
|
||||
desc string, resolvable bool, timeout time.Duration, opts ...RequestOption) (*spoof.Response, error) {
|
||||
kubeClient *KubeClient,
|
||||
logf logging.FormatLogger,
|
||||
theURL string,
|
||||
inState spoof.ResponseChecker,
|
||||
desc string,
|
||||
resolvable bool,
|
||||
timeout time.Duration,
|
||||
opts ...interface{}) (*spoof.Response, error) {
|
||||
defer logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForEndpointState/%s", desc)).End()
|
||||
|
||||
// Try parsing the "theURL" with and without a scheme.
|
||||
|
@ -163,11 +176,17 @@ func WaitForEndpointStateWithTimeout(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var tOpts []spoof.TransportOption
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
rOpt, ok := opt.(RequestOption)
|
||||
if ok {
|
||||
rOpt(req)
|
||||
} else if tOpt, ok := opt.(spoof.TransportOption); ok {
|
||||
tOpts = append(tOpts, tOpt)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := NewSpoofingClient(kubeClient, logf, asURL.Hostname(), resolvable)
|
||||
client, err := NewSpoofingClient(kubeClient, logf, asURL.Hostname(), resolvable, tOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -19,9 +19,12 @@ limitations under the License.
|
|||
package spoof
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -66,7 +69,10 @@ type Interface interface {
|
|||
}
|
||||
|
||||
// https://medium.com/stupid-gopher-tricks/ensuring-go-interface-satisfaction-at-compile-time-1ed158e8fa17
|
||||
var _ Interface = (*SpoofingClient)(nil)
|
||||
var (
|
||||
_ Interface = (*SpoofingClient)(nil)
|
||||
dialContext = (&net.Dialer{}).DialContext
|
||||
)
|
||||
|
||||
// ResponseChecker is used to determine when SpoofinClient.Poll is done polling.
|
||||
// This allows you to predicate wait.PollImmediate on the request's http.Response.
|
||||
|
@ -82,72 +88,81 @@ type SpoofingClient struct {
|
|||
RequestInterval time.Duration
|
||||
RequestTimeout time.Duration
|
||||
|
||||
endpoint string
|
||||
domain string
|
||||
|
||||
logf logging.FormatLogger
|
||||
}
|
||||
|
||||
// New returns a SpoofingClient that rewrites requests if the target domain is not `resolveable`.
|
||||
// TransportOption allows callers to customize the http.Transport used by a SpoofingClient
|
||||
type TransportOption func(transport *http.Transport) *http.Transport
|
||||
|
||||
// New returns a SpoofingClient that rewrites requests if the target domain is not `resolvable`.
|
||||
// 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, logf logging.FormatLogger, domain string, resolvable bool, endpointOverride string) (*SpoofingClient, error) {
|
||||
func New(
|
||||
kubeClientset *kubernetes.Clientset,
|
||||
logf logging.FormatLogger,
|
||||
domain string,
|
||||
resolvable bool,
|
||||
endpointOverride string,
|
||||
opts ...TransportOption) (*SpoofingClient, error) {
|
||||
endpoint, err := ResolveEndpoint(kubeClientset, domain, resolvable, endpointOverride)
|
||||
if err != nil {
|
||||
fmt.Errorf("failed get the cluster endpoint: %v", err)
|
||||
}
|
||||
|
||||
// Spoof the hostname at the resolver level
|
||||
transport := &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, e error) {
|
||||
spoofed := addr
|
||||
if i := strings.LastIndex(addr, ":"); i != -1 && domain == addr[:i] {
|
||||
// The original hostname:port is spoofed by replacing the hostname by the value
|
||||
// returned by ResolveEndpoint.
|
||||
spoofed = endpoint + ":" + addr[i+1:]
|
||||
logf("Spoofing %s -> %s", addr, spoofed)
|
||||
}
|
||||
return dialContext(ctx, network, spoofed)
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
transport = opt(transport)
|
||||
}
|
||||
|
||||
// Enable Zipkin tracing
|
||||
roundTripper := &ochttp.Transport{
|
||||
Base: transport,
|
||||
Propagation: &b3.HTTPFormat{},
|
||||
}
|
||||
|
||||
sc := SpoofingClient{
|
||||
Client: &http.Client{Transport: &ochttp.Transport{Propagation: &b3.HTTPFormat{}}}, // Using ochttp Transport required for zipkin-tracing
|
||||
Client: &http.Client{Transport: roundTripper},
|
||||
RequestInterval: requestInterval,
|
||||
RequestTimeout: RequestTimeout,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
var err error
|
||||
if sc.endpoint, err = ResolveEndpoint(kubeClientset, domain, resolvable, endpointOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !resolvable {
|
||||
sc.domain = domain
|
||||
}
|
||||
|
||||
return &sc, nil
|
||||
}
|
||||
|
||||
// ResolveEndpoint resolves the endpoint address considering whether the domain is resolvable and taking into
|
||||
// account whether the user overrode the endpoint address externally
|
||||
func ResolveEndpoint(kubeClientset *kubernetes.Clientset, domain string, resolvable bool, endpointOverride string) (string, error) {
|
||||
// If the domain is resolvable, we can use it directly when we make requests.
|
||||
endpoint := domain
|
||||
if !resolvable {
|
||||
e := endpointOverride
|
||||
if endpointOverride == "" {
|
||||
var err error
|
||||
// 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 endpoint and spoof
|
||||
// the Host in our requests.
|
||||
if e, err = ingress.GetIngressEndpoint(kubeClientset); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
endpoint = e
|
||||
// If the domain is resolvable, it can be used directly
|
||||
if resolvable {
|
||||
return domain, nil
|
||||
}
|
||||
return endpoint, nil
|
||||
// If an override is provided, use it
|
||||
if endpointOverride != "" {
|
||||
return endpointOverride, nil
|
||||
}
|
||||
// Otherwise, use the actual cluster endpoint
|
||||
return ingress.GetIngressEndpoint(kubeClientset)
|
||||
}
|
||||
|
||||
// Do dispatches to the underlying http.Client.Do, spoofing domains as needed
|
||||
// and transforming the http.Response into a spoof.Response.
|
||||
// Each response is augmented with "ZipkinTraceID" header that identifies the zipkin trace corresponding to the request.
|
||||
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
|
||||
}
|
||||
|
||||
// Starting span to capture zipkin trace.
|
||||
traceContext, span := trace.StartSpan(req.Context(), "SpoofingClient-Trace")
|
||||
defer span.End()
|
||||
|
|
|
@ -21,6 +21,14 @@ type CoverageValues struct {
|
|||
TotalFields int
|
||||
CoveredFields int
|
||||
IgnoredFields int
|
||||
|
||||
PercentCoverage float64
|
||||
}
|
||||
|
||||
func (c *CoverageValues) CalculatePercentageValue() {
|
||||
if c.TotalFields > 0 {
|
||||
c.PercentCoverage = (float64(c.CoveredFields) / float64(c.TotalFields-c.IgnoredFields)) * 100
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTypeCoverage calculates aggregate coverage values based on provided []TypeCoverage
|
||||
|
|
|
@ -16,7 +16,10 @@ limitations under the License.
|
|||
|
||||
package coveragecalculator
|
||||
|
||||
import "k8s.io/apimachinery/pkg/util/sets"
|
||||
import (
|
||||
"strings"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
// FieldCoverage represents coverage data for a field.
|
||||
type FieldCoverage struct {
|
||||
|
@ -43,6 +46,11 @@ func (f *FieldCoverage) GetValues() []string {
|
|||
return values
|
||||
}
|
||||
|
||||
// GetValuesForDisplay returns value strings as comma separated string.
|
||||
func (f *FieldCoverage) GetValuesForDisplay() string {
|
||||
return strings.Join(f.GetValues(), ",")
|
||||
}
|
||||
|
||||
// TypeCoverage encapsulates type information and field coverage.
|
||||
type TypeCoverage struct {
|
||||
Package string `json:"Package"`
|
||||
|
|
|
@ -24,7 +24,6 @@ import (
|
|||
"net/http"
|
||||
"os/user"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
|
||||
"knative.dev/pkg/test/webhook-apicoverage/view"
|
||||
|
@ -177,13 +176,12 @@ func GetAndWriteTotalCoverage(webhookIP string, outputFile string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(view.HTMLHeader)
|
||||
totalCoverageDisplay := view.GetHTMLCoverageValuesDisplay(totalCoverage)
|
||||
buffer.WriteString(totalCoverageDisplay)
|
||||
buffer.WriteString(view.HTMLFooter)
|
||||
if err = ioutil.WriteFile(outputFile, []byte(buffer.String()), 0400); err != nil {
|
||||
return fmt.Errorf("error writing total coverage to output file: %s, error: %v coverage: %s", outputFile, err, totalCoverageDisplay)
|
||||
if htmlData, err := view.GetHTMLCoverageValuesDisplay(totalCoverage); err != nil {
|
||||
return fmt.Errorf("error building html file from total coverage. error: %v", err)
|
||||
} else {
|
||||
if err = ioutil.WriteFile(outputFile, []byte(htmlData), 0400); err != nil {
|
||||
return fmt.Errorf("error writing total coverage to output file: %s, error: %v", outputFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
25
vendor/knative.dev/pkg/test/webhook-apicoverage/view/aggregate_coverage.html
vendored
Normal file
25
vendor/knative.dev/pkg/test/webhook-apicoverage/view/aggregate_coverage.html
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<style type="text/css">
|
||||
<!--
|
||||
|
||||
.tab { margin-left: 50px; }
|
||||
|
||||
.styleheader {color: white; size: A4}
|
||||
|
||||
.values {color: yellow; size: A3}
|
||||
|
||||
table, th, td { border: 1px solid white; text-align: center}
|
||||
|
||||
.braces {color: white; size: A3}
|
||||
-->
|
||||
</style>
|
||||
<body style="background-color:rgb(0,0,0); font-family: Arial">
|
||||
<table style="width: 30%">
|
||||
<tr class="styleheader"><td>Total Fields</td><td>{{ .TotalFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Covered Fields</td><td>{{ .CoveredFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Ignored Fields</td><td>{{ .IgnoredFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Coverage Percentage</td><td>{{ .PercentCoverage }}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -17,115 +17,53 @@ limitations under the License.
|
|||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"html/template"
|
||||
|
||||
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
|
||||
)
|
||||
|
||||
// HTMLHeader sets up an HTML page with the right style format
|
||||
var HTMLHeader = fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<style type="text/css">
|
||||
<!--
|
||||
|
||||
.tab { margin-left: 50px; }
|
||||
|
||||
.styleheader {color: white; size: A4}
|
||||
|
||||
.covered {color: green; size: A3}
|
||||
|
||||
.notcovered {color: red; size: A3}
|
||||
|
||||
.ignored {color: white; size: A4}
|
||||
|
||||
.values {color: yellow; size: A3}
|
||||
|
||||
table, th, td { border: 1px solid white; text-align: center}
|
||||
|
||||
.braces {color: white; size: A3}
|
||||
-->
|
||||
</style>
|
||||
<body style="background-color:rgb(0,0,0); font-family: Arial">
|
||||
`)
|
||||
|
||||
// HTMLFooter closes the tags for the HTML page.
|
||||
var HTMLFooter = fmt.Sprintf(`
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
type HtmlDisplayData struct {
|
||||
TypeCoverages []coveragecalculator.TypeCoverage
|
||||
CoverageNumbers *coveragecalculator.CoverageValues
|
||||
}
|
||||
|
||||
// GetHTMLDisplay is a helper method to display API Coverage details in json-like format inside a HTML page.
|
||||
func GetHTMLDisplay(coverageData []coveragecalculator.TypeCoverage, displayRules DisplayRules) string {
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(HTMLHeader)
|
||||
for _, typeCoverage := range coverageData {
|
||||
packageName := typeCoverage.Package
|
||||
if displayRules.PackageNameRule != nil {
|
||||
packageName = displayRules.PackageNameRule(packageName)
|
||||
}
|
||||
|
||||
typeName := typeCoverage.Type
|
||||
if displayRules.TypeNameRule != nil {
|
||||
typeName = displayRules.TypeNameRule(typeName)
|
||||
}
|
||||
buffer.WriteString(fmt.Sprint(`<div class="styleheader">`))
|
||||
buffer.WriteString(fmt.Sprintf(`<br>Package: %s`, packageName))
|
||||
buffer.WriteString(fmt.Sprintf(`<br>Type: %s`, typeName))
|
||||
buffer.WriteString(fmt.Sprint(`<br></div>`))
|
||||
buffer.WriteString(fmt.Sprint(`<div class="braces"><br>{</div>`))
|
||||
for _, fieldCoverage := range typeCoverage.Fields {
|
||||
var fieldDisplay string
|
||||
if displayRules.FieldRule != nil {
|
||||
fieldDisplay = displayRules.FieldRule(fieldCoverage)
|
||||
} else {
|
||||
fieldDisplay = defaultHTMLTypeDisplay(fieldCoverage)
|
||||
}
|
||||
buffer.WriteString(fieldDisplay)
|
||||
}
|
||||
|
||||
buffer.WriteString(fmt.Sprint(`<div class="braces">}</div>`))
|
||||
buffer.WriteString(fmt.Sprint(`<br>`))
|
||||
func GetHTMLDisplay(coverageData []coveragecalculator.TypeCoverage, coverageValues *coveragecalculator.CoverageValues) (string, error) {
|
||||
|
||||
htmlData := HtmlDisplayData{
|
||||
TypeCoverages: coverageData,
|
||||
CoverageNumbers: coverageValues,
|
||||
}
|
||||
|
||||
buffer.WriteString(HTMLFooter)
|
||||
return buffer.String()
|
||||
tmpl, err := template.ParseFiles("type_coverage.html")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buffer strings.Builder
|
||||
err = tmpl.Execute(&buffer, htmlData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
func defaultHTMLTypeDisplay(field *coveragecalculator.FieldCoverage) string {
|
||||
var buffer strings.Builder
|
||||
if field.Ignored {
|
||||
buffer.WriteString(fmt.Sprintf(`<div class="ignored tab">%s</div>`, field.Field))
|
||||
} else if field.Coverage {
|
||||
buffer.WriteString(fmt.Sprintf(`<div class="covered tab">%s`, field.Field))
|
||||
if len(field.Values) > 0 && !strings.Contains(strings.ToLower(field.Field), "uid") {
|
||||
buffer.WriteString(fmt.Sprintf(`    <span class="values">Values: [%s]</span>`, strings.Join(field.GetValues(), ", ")))
|
||||
}
|
||||
buffer.WriteString(fmt.Sprint(`</div>`))
|
||||
} else {
|
||||
buffer.WriteString(fmt.Sprintf(`<div class="notcovered tab">%s</div>`, field.Field))
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// GetHTMLCoverageValuesDisplay is a helper method to display coverage values inside a HTML table.
|
||||
func GetHTMLCoverageValuesDisplay(coverageValues *coveragecalculator.CoverageValues) string {
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(fmt.Sprint(`<br>`))
|
||||
buffer.WriteString(fmt.Sprint(`<div class="styleheader">Coverage Values</div>`))
|
||||
buffer.WriteString(fmt.Sprint(`<br>`))
|
||||
buffer.WriteString(fmt.Sprint(` <table style="width: 30%">`))
|
||||
buffer.WriteString(fmt.Sprintf(`<tr class="styleheader"><td>Total Fields</td><td>%d</td></tr>`, coverageValues.TotalFields))
|
||||
buffer.WriteString(fmt.Sprintf(`<tr class="styleheader"><td>Covered Fields</td><td>%d</td></tr>`, coverageValues.CoveredFields))
|
||||
buffer.WriteString(fmt.Sprintf(`<tr class="styleheader"><td>Ignored Fields</td><td>%d</td></tr>`, coverageValues.IgnoredFields))
|
||||
func GetHTMLCoverageValuesDisplay(coverageValues *coveragecalculator.CoverageValues) (string, error) {
|
||||
|
||||
percentCoverage := 0.0
|
||||
if coverageValues.TotalFields > 0 {
|
||||
percentCoverage = (float64(coverageValues.CoveredFields) / float64(coverageValues.TotalFields-coverageValues.IgnoredFields)) * 100
|
||||
tmpl, err := template.ParseFiles("aggregate_coverage.html")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buffer.WriteString(fmt.Sprintf(`<tr class="styleheader"><td>Coverage Percentage</td><td>%f</td></tr>`, percentCoverage))
|
||||
buffer.WriteString(fmt.Sprint(`</table>`))
|
||||
return buffer.String()
|
||||
|
||||
var buffer strings.Builder
|
||||
err = tmpl.Execute(&buffer, coverageValues)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<style type="text/css">
|
||||
<!--
|
||||
|
||||
.tab { margin-left: 50px; }
|
||||
|
||||
.styleheader {color: white; size: A4}
|
||||
|
||||
.covered {color: green; size: A3}
|
||||
|
||||
.notcovered {color: red; size: A3}
|
||||
|
||||
.ignored {color: white; size: A4}
|
||||
|
||||
.values {color: yellow; size: A3}
|
||||
|
||||
table, th, td { border: 1px solid white; text-align: center}
|
||||
|
||||
.braces {color: white; size: A3}
|
||||
-->
|
||||
</style>
|
||||
<body style="background-color:rgb(0,0,0); font-family: Arial">
|
||||
{{ range $coverageType := .TypeCoverages }}
|
||||
<div class="styleheader">
|
||||
<br>Package: {{ $coverageType.Package }}
|
||||
<br>Type: {{ $coverageType.Type }}
|
||||
<br>
|
||||
<div class="braces">
|
||||
<br>{
|
||||
</div>
|
||||
{{ range $key, $value := $coverageType.Fields }}
|
||||
{{if $value.Ignored }}
|
||||
<div class="ignored tab">{{ $value.Field }}</div>
|
||||
{{else if $value.Coverage}}
|
||||
<div class="covered tab">{{ $value.Field }}
|
||||
{{ $valueLen := len $value.Values }}
|
||||
{{if gt $valueLen 0 }}
|
||||
    <span class="values">Values: [{{$value.GetValuesForDisplay}}]</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="notcovered tab">{{ $value.Field }}</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="braces">}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<div class="styleheader">Coverage Values</div>
|
||||
<br>
|
||||
<table style="width: 30%">
|
||||
<tr class="styleheader"><td>Total Fields</td><td>{{ .CoverageNumbers.TotalFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Covered Fields</td><td>{{ .CoverageNumbers.CoveredFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Ignored Fields</td><td>{{ .CoverageNumbers.IgnoredFields }}</td></tr>
|
||||
<tr class="styleheader"><td>Coverage Percentage</td><td>{{ .CoverageNumbers.PercentCoverage }}</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -23,7 +23,6 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"k8s.io/api/admission/v1beta1"
|
||||
|
@ -168,11 +167,13 @@ func (a *APICoverageRecorder) GetResourceCoverage(w http.ResponseWriter, r *http
|
|||
tree := a.ResourceForest.TopLevelTrees[resource]
|
||||
typeCoverage := tree.BuildCoverageData(a.NodeRules, a.FieldRules, ignoredFields)
|
||||
coverageValues := coveragecalculator.CalculateTypeCoverage(typeCoverage)
|
||||
coverageValues.CalculatePercentageValue()
|
||||
|
||||
var buffer strings.Builder
|
||||
buffer.WriteString(view.GetHTMLDisplay(typeCoverage, a.DisplayRules))
|
||||
buffer.WriteString(view.GetHTMLCoverageValuesDisplay(coverageValues))
|
||||
fmt.Fprint(w, buffer.String())
|
||||
if htmlData, err := view.GetHTMLDisplay(typeCoverage, coverageValues); err != nil {
|
||||
fmt.Fprintf(w, "Error generating html file %v", err)
|
||||
} else {
|
||||
fmt.Fprint(w, htmlData)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTotalCoverage goes over all the resources setup for the apicoverage tool and returns total coverage values.
|
||||
|
@ -197,6 +198,7 @@ func (a *APICoverageRecorder) GetTotalCoverage(w http.ResponseWriter, r *http.Re
|
|||
totalCoverage.IgnoredFields += coverageValues.IgnoredFields
|
||||
}
|
||||
|
||||
totalCoverage.CalculatePercentageValue()
|
||||
var body []byte
|
||||
if body, err = json.Marshal(totalCoverage); err != nil {
|
||||
fmt.Fprintf(w, "error marshalling total coverage response: %v", err)
|
||||
|
|
|
@ -60,11 +60,24 @@ type GenericCRD interface {
|
|||
runtime.Object
|
||||
}
|
||||
|
||||
// ResourceAdmissionController implements the AdmissionController for resources
|
||||
type ResourceAdmissionController struct {
|
||||
Handlers map[schema.GroupVersionKind]GenericCRD
|
||||
Options ControllerOptions
|
||||
handlers map[schema.GroupVersionKind]GenericCRD
|
||||
options ControllerOptions
|
||||
|
||||
DisallowUnknownFields bool
|
||||
disallowUnknownFields bool
|
||||
}
|
||||
|
||||
// NewResourceAdmissionController constructs a ResourceAdmissionController
|
||||
func NewResourceAdmissionController(
|
||||
handlers map[schema.GroupVersionKind]GenericCRD,
|
||||
opts ControllerOptions,
|
||||
disallowUnknownFields bool) AdmissionController {
|
||||
return &ResourceAdmissionController{
|
||||
handlers: handlers,
|
||||
options: opts,
|
||||
disallowUnknownFields: disallowUnknownFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *ResourceAdmissionController) Admit(ctx context.Context, request *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse {
|
||||
|
@ -98,7 +111,7 @@ func (ac *ResourceAdmissionController) Register(ctx context.Context, kubeClient
|
|||
failurePolicy := admissionregistrationv1beta1.Fail
|
||||
|
||||
var rules []admissionregistrationv1beta1.RuleWithOperations
|
||||
for gvk := range ac.Handlers {
|
||||
for gvk := range ac.handlers {
|
||||
plural := strings.ToLower(inflect.Pluralize(gvk.Kind))
|
||||
|
||||
rules = append(rules, admissionregistrationv1beta1.RuleWithOperations{
|
||||
|
@ -128,15 +141,16 @@ func (ac *ResourceAdmissionController) Register(ctx context.Context, kubeClient
|
|||
|
||||
webhook := &admissionregistrationv1beta1.MutatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: ac.Options.WebhookName,
|
||||
Name: ac.options.WebhookName,
|
||||
},
|
||||
Webhooks: []admissionregistrationv1beta1.Webhook{{
|
||||
Name: ac.Options.WebhookName,
|
||||
Name: ac.options.WebhookName,
|
||||
Rules: rules,
|
||||
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
|
||||
Service: &admissionregistrationv1beta1.ServiceReference{
|
||||
Namespace: ac.Options.Namespace,
|
||||
Name: ac.Options.ServiceName,
|
||||
Namespace: ac.options.Namespace,
|
||||
Name: ac.options.ServiceName,
|
||||
Path: &ac.options.ResourceAdmissionControllerPath,
|
||||
},
|
||||
CABundle: caCert,
|
||||
},
|
||||
|
@ -145,7 +159,7 @@ func (ac *ResourceAdmissionController) Register(ctx context.Context, kubeClient
|
|||
}
|
||||
|
||||
// Set the owner to our deployment.
|
||||
deployment, err := kubeClient.Apps().Deployments(ac.Options.Namespace).Get(ac.Options.DeploymentName, metav1.GetOptions{})
|
||||
deployment, err := kubeClient.Apps().Deployments(ac.options.Namespace).Get(ac.options.DeploymentName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch our deployment: %v", err)
|
||||
}
|
||||
|
@ -159,7 +173,7 @@ func (ac *ResourceAdmissionController) Register(ctx context.Context, kubeClient
|
|||
return fmt.Errorf("failed to create a webhook: %v", err)
|
||||
}
|
||||
logger.Info("Webhook already exists")
|
||||
configuredWebhook, err := client.Get(ac.Options.WebhookName, metav1.GetOptions{})
|
||||
configuredWebhook, err := client.Get(ac.options.WebhookName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving webhook: %v", err)
|
||||
}
|
||||
|
@ -193,7 +207,7 @@ func (ac *ResourceAdmissionController) mutate(ctx context.Context, req *admissio
|
|||
}
|
||||
|
||||
logger := logging.FromContext(ctx)
|
||||
handler, ok := ac.Handlers[gvk]
|
||||
handler, ok := ac.handlers[gvk]
|
||||
if !ok {
|
||||
logger.Errorf("Unhandled kind: %v", gvk)
|
||||
return nil, fmt.Errorf("unhandled kind: %v", gvk)
|
||||
|
@ -205,7 +219,7 @@ func (ac *ResourceAdmissionController) mutate(ctx context.Context, req *admissio
|
|||
if len(newBytes) != 0 {
|
||||
newObj = handler.DeepCopyObject().(GenericCRD)
|
||||
newDecoder := json.NewDecoder(bytes.NewBuffer(newBytes))
|
||||
if ac.DisallowUnknownFields {
|
||||
if ac.disallowUnknownFields {
|
||||
newDecoder.DisallowUnknownFields()
|
||||
}
|
||||
if err := newDecoder.Decode(&newObj); err != nil {
|
||||
|
@ -215,7 +229,7 @@ func (ac *ResourceAdmissionController) mutate(ctx context.Context, req *admissio
|
|||
if len(oldBytes) != 0 {
|
||||
oldObj = handler.DeepCopyObject().(GenericCRD)
|
||||
oldDecoder := json.NewDecoder(bytes.NewBuffer(oldBytes))
|
||||
if ac.DisallowUnknownFields {
|
||||
if ac.disallowUnknownFields {
|
||||
oldDecoder.DisallowUnknownFields()
|
||||
}
|
||||
if err := oldDecoder.Decode(&oldObj); err != nil {
|
||||
|
@ -323,3 +337,41 @@ func roundTripPatch(bytes []byte, unmarshalled interface{}) (duck.JSONPatch, err
|
|||
}
|
||||
return jsonpatch.CreatePatch(bytes, marshaledBytes)
|
||||
}
|
||||
|
||||
// validate performs validation on the provided "new" CRD.
|
||||
// For legacy purposes, this also does apis.Immutable validation,
|
||||
// which is deprecated and will be removed in a future release.
|
||||
func validate(ctx context.Context, new apis.Validatable) error {
|
||||
if apis.IsInUpdate(ctx) {
|
||||
old := apis.GetBaseline(ctx)
|
||||
if immutableNew, ok := new.(apis.Immutable); ok {
|
||||
immutableOld, ok := old.(apis.Immutable)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type mismatch %T vs. %T", old, new)
|
||||
}
|
||||
if err := immutableNew.CheckImmutableFields(ctx, immutableOld); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can't just `return new.Validate()` because it doesn't properly nil-check.
|
||||
if err := new.Validate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaults simply leverages apis.Defaultable to set defaults.
|
||||
func setDefaults(ctx context.Context, patches duck.JSONPatch, crd GenericCRD) (duck.JSONPatch, error) {
|
||||
before, after := crd.DeepCopyObject(), crd
|
||||
after.SetDefaults(ctx)
|
||||
|
||||
patch, err := duck.CreatePatch(before, after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(patches, patch...), nil
|
||||
}
|
||||
|
|
|
@ -28,8 +28,6 @@ import (
|
|||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"knative.dev/pkg/apis"
|
||||
"knative.dev/pkg/apis/duck"
|
||||
"knative.dev/pkg/logging"
|
||||
"knative.dev/pkg/logging/logkey"
|
||||
|
||||
|
@ -38,7 +36,6 @@ import (
|
|||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
|
@ -94,27 +91,37 @@ type ControllerOptions struct {
|
|||
// StatsReporter reports metrics about the webhook.
|
||||
// This will be automatically initialized by the constructor if left uninitialized.
|
||||
StatsReporter StatsReporter
|
||||
|
||||
// Service path for ResourceAdmissionController webhook
|
||||
// Default is "/" for backward compatibility and is set by the constructor
|
||||
ResourceAdmissionControllerPath string
|
||||
}
|
||||
|
||||
// AdmissionController implements the external admission webhook for validation of
|
||||
// pilot configuration.
|
||||
type AdmissionController struct {
|
||||
Client kubernetes.Interface
|
||||
Options ControllerOptions
|
||||
Logger *zap.SugaredLogger
|
||||
resourceAdmissionController ResourceAdmissionController
|
||||
// AdmissionController provides the interface for different admission controllers
|
||||
type AdmissionController interface {
|
||||
Admit(context.Context, *admissionv1beta1.AdmissionRequest) *admissionv1beta1.AdmissionResponse
|
||||
Register(context.Context, kubernetes.Interface, []byte) error
|
||||
}
|
||||
|
||||
// Webhook implements the external webhook for validation of
|
||||
// resources and configuration.
|
||||
type Webhook struct {
|
||||
Client kubernetes.Interface
|
||||
Options ControllerOptions
|
||||
Logger *zap.SugaredLogger
|
||||
admissionControllers map[string]AdmissionController
|
||||
|
||||
WithContext func(context.Context) context.Context
|
||||
}
|
||||
|
||||
// NewAdmissionController constructs an AdmissionController
|
||||
func NewAdmissionController(
|
||||
// New constructs a Webhook
|
||||
func New(
|
||||
client kubernetes.Interface,
|
||||
opts ControllerOptions,
|
||||
handlers map[schema.GroupVersionKind]GenericCRD,
|
||||
admissionControllers map[string]AdmissionController,
|
||||
logger *zap.SugaredLogger,
|
||||
ctx func(context.Context) context.Context,
|
||||
disallowUnknownFields bool) (*AdmissionController, error) {
|
||||
) (*Webhook, error) {
|
||||
|
||||
if opts.StatsReporter == nil {
|
||||
reporter, err := NewStatsReporter()
|
||||
|
@ -124,19 +131,126 @@ func NewAdmissionController(
|
|||
opts.StatsReporter = reporter
|
||||
}
|
||||
|
||||
return &AdmissionController{
|
||||
Client: client,
|
||||
Options: opts,
|
||||
resourceAdmissionController: ResourceAdmissionController{
|
||||
Handlers: handlers,
|
||||
Options: opts,
|
||||
DisallowUnknownFields: disallowUnknownFields,
|
||||
},
|
||||
Logger: logger,
|
||||
WithContext: ctx,
|
||||
return &Webhook{
|
||||
Client: client,
|
||||
Options: opts,
|
||||
admissionControllers: admissionControllers,
|
||||
Logger: logger,
|
||||
WithContext: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run implements the admission controller run loop.
|
||||
func (ac *Webhook) Run(stop <-chan struct{}) error {
|
||||
logger := ac.Logger
|
||||
ctx := logging.WithLogger(context.TODO(), logger)
|
||||
tlsConfig, caCert, err := configureCerts(ctx, ac.Client, &ac.Options)
|
||||
if err != nil {
|
||||
logger.Errorw("could not configure admission webhook certs", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: ac,
|
||||
Addr: fmt.Sprintf(":%v", ac.Options.Port),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
logger.Info("Found certificates for webhook...")
|
||||
if ac.Options.RegistrationDelay != 0 {
|
||||
logger.Infof("Delaying admission webhook registration for %v", ac.Options.RegistrationDelay)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(ac.Options.RegistrationDelay):
|
||||
for _, c := range ac.admissionControllers {
|
||||
if err := c.Register(ctx, ac.Client, caCert); err != nil {
|
||||
logger.Errorw("failed to register webhook", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
logger.Info("Successfully registered webhook")
|
||||
case <-stop:
|
||||
return nil
|
||||
}
|
||||
|
||||
serverBootstrapErrCh := make(chan struct{})
|
||||
go func() {
|
||||
if err := server.ListenAndServeTLS("", ""); err != nil {
|
||||
logger.Errorw("ListenAndServeTLS for admission webhook returned error", zap.Error(err))
|
||||
close(serverBootstrapErrCh)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return server.Close()
|
||||
case <-serverBootstrapErrCh:
|
||||
return errors.New("webhook server bootstrap failed")
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP implements the external admission webhook for mutating
|
||||
// serving resources.
|
||||
func (ac *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var ttStart = time.Now()
|
||||
logger := ac.Logger
|
||||
logger.Infof("Webhook ServeHTTP request=%#v", r)
|
||||
|
||||
// Verify the content type is accurate.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
var review admissionv1beta1.AdmissionReview
|
||||
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not decode body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logger = logger.With(
|
||||
zap.String(logkey.Kind, fmt.Sprint(review.Request.Kind)),
|
||||
zap.String(logkey.Namespace, review.Request.Namespace),
|
||||
zap.String(logkey.Name, review.Request.Name),
|
||||
zap.String(logkey.Operation, fmt.Sprint(review.Request.Operation)),
|
||||
zap.String(logkey.Resource, fmt.Sprint(review.Request.Resource)),
|
||||
zap.String(logkey.SubResource, fmt.Sprint(review.Request.SubResource)),
|
||||
zap.String(logkey.UserInfo, fmt.Sprint(review.Request.UserInfo)))
|
||||
ctx := logging.WithLogger(r.Context(), logger)
|
||||
|
||||
if ac.WithContext != nil {
|
||||
ctx = ac.WithContext(ctx)
|
||||
}
|
||||
|
||||
if _, ok := ac.admissionControllers[r.URL.Path]; !ok {
|
||||
http.Error(w, fmt.Sprintf("no admission controller registered for: %s", r.URL.Path), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c := ac.admissionControllers[r.URL.Path]
|
||||
reviewResponse := c.Admit(ctx, review.Request)
|
||||
var response admissionv1beta1.AdmissionReview
|
||||
if reviewResponse != nil {
|
||||
response.Response = reviewResponse
|
||||
response.Response.UID = review.Request.UID
|
||||
}
|
||||
|
||||
logger.Infof("AdmissionReview for %#v: %s/%s response=%#v",
|
||||
review.Request.Kind, review.Request.Namespace, review.Request.Name, reviewResponse)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, fmt.Sprintf("could encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ac.Options.StatsReporter != nil {
|
||||
// Only report valid requests
|
||||
ac.Options.StatsReporter.ReportRequest(review.Request, response.Response, time.Since(ttStart))
|
||||
}
|
||||
}
|
||||
|
||||
// GetAPIServerExtensionCACert gets the Kubernetes aggregate apiserver
|
||||
// client CA cert used by validator.
|
||||
//
|
||||
|
@ -210,44 +324,6 @@ func getOrGenerateKeyCertsFromSecret(ctx context.Context, client kubernetes.Inte
|
|||
return serverKey, serverCert, caCert, nil
|
||||
}
|
||||
|
||||
// validate performs validation on the provided "new" CRD.
|
||||
// For legacy purposes, this also does apis.Immutable validation,
|
||||
// which is deprecated and will be removed in a future release.
|
||||
func validate(ctx context.Context, new apis.Validatable) error {
|
||||
if apis.IsInUpdate(ctx) {
|
||||
old := apis.GetBaseline(ctx)
|
||||
if immutableNew, ok := new.(apis.Immutable); ok {
|
||||
immutableOld, ok := old.(apis.Immutable)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type mismatch %T vs. %T", old, new)
|
||||
}
|
||||
if err := immutableNew.CheckImmutableFields(ctx, immutableOld); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can't just `return new.Validate()` because it doesn't properly nil-check.
|
||||
if err := new.Validate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setDefaults simply leverages apis.Defaultable to set defaults.
|
||||
func setDefaults(ctx context.Context, patches duck.JSONPatch, crd GenericCRD) (duck.JSONPatch, error) {
|
||||
before, after := crd.DeepCopyObject(), crd
|
||||
after.SetDefaults(ctx)
|
||||
|
||||
patch, err := duck.CreatePatch(before, after)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(patches, patch...), nil
|
||||
}
|
||||
|
||||
func configureCerts(ctx context.Context, client kubernetes.Interface, options *ControllerOptions) (*tls.Config, []byte, error) {
|
||||
var apiServerCACert []byte
|
||||
if options.ClientAuth >= tls.VerifyClientCertIfGiven {
|
||||
|
@ -269,109 +345,6 @@ func configureCerts(ctx context.Context, client kubernetes.Interface, options *C
|
|||
return tlsConfig, caCert, nil
|
||||
}
|
||||
|
||||
// Run implements the admission controller run loop.
|
||||
func (ac *AdmissionController) Run(stop <-chan struct{}) error {
|
||||
logger := ac.Logger
|
||||
ctx := logging.WithLogger(context.TODO(), logger)
|
||||
tlsConfig, caCert, err := configureCerts(ctx, ac.Client, &ac.Options)
|
||||
if err != nil {
|
||||
logger.Errorw("could not configure admission webhook certs", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Handler: ac,
|
||||
Addr: fmt.Sprintf(":%v", ac.Options.Port),
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
|
||||
logger.Info("Found certificates for webhook...")
|
||||
if ac.Options.RegistrationDelay != 0 {
|
||||
logger.Infof("Delaying admission webhook registration for %v", ac.Options.RegistrationDelay)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(ac.Options.RegistrationDelay):
|
||||
if err := ac.resourceAdmissionController.Register(ctx, ac.Client, caCert); err != nil {
|
||||
logger.Errorw("failed to register webhook", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
logger.Info("Successfully registered webhook")
|
||||
case <-stop:
|
||||
return nil
|
||||
}
|
||||
|
||||
serverBootstrapErrCh := make(chan struct{})
|
||||
go func() {
|
||||
if err := server.ListenAndServeTLS("", ""); err != nil {
|
||||
logger.Errorw("ListenAndServeTLS for admission webhook returned error", zap.Error(err))
|
||||
close(serverBootstrapErrCh)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-stop:
|
||||
return server.Close()
|
||||
case <-serverBootstrapErrCh:
|
||||
return errors.New("webhook server bootstrap failed")
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP implements the external admission webhook for mutating
|
||||
// serving resources.
|
||||
func (ac *AdmissionController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var ttStart = time.Now()
|
||||
logger := ac.Logger
|
||||
logger.Infof("Webhook ServeHTTP request=%#v", r)
|
||||
|
||||
// Verify the content type is accurate.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/json" {
|
||||
http.Error(w, "invalid Content-Type, want `application/json`", http.StatusUnsupportedMediaType)
|
||||
return
|
||||
}
|
||||
|
||||
var review admissionv1beta1.AdmissionReview
|
||||
if err := json.NewDecoder(r.Body).Decode(&review); err != nil {
|
||||
http.Error(w, fmt.Sprintf("could not decode body: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logger = logger.With(
|
||||
zap.String(logkey.Kind, fmt.Sprint(review.Request.Kind)),
|
||||
zap.String(logkey.Namespace, review.Request.Namespace),
|
||||
zap.String(logkey.Name, review.Request.Name),
|
||||
zap.String(logkey.Operation, fmt.Sprint(review.Request.Operation)),
|
||||
zap.String(logkey.Resource, fmt.Sprint(review.Request.Resource)),
|
||||
zap.String(logkey.SubResource, fmt.Sprint(review.Request.SubResource)),
|
||||
zap.String(logkey.UserInfo, fmt.Sprint(review.Request.UserInfo)))
|
||||
ctx := logging.WithLogger(r.Context(), logger)
|
||||
|
||||
if ac.WithContext != nil {
|
||||
ctx = ac.WithContext(ctx)
|
||||
}
|
||||
|
||||
reviewResponse := ac.resourceAdmissionController.Admit(ctx, review.Request)
|
||||
var response admissionv1beta1.AdmissionReview
|
||||
if reviewResponse != nil {
|
||||
response.Response = reviewResponse
|
||||
response.Response.UID = review.Request.UID
|
||||
}
|
||||
|
||||
logger.Infof("AdmissionReview for %#v: %s/%s response=%#v",
|
||||
review.Request.Kind, review.Request.Namespace, review.Request.Name, reviewResponse)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, fmt.Sprintf("could encode response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ac.Options.StatsReporter != nil {
|
||||
// Only report valid requests
|
||||
ac.Options.StatsReporter.ReportRequest(review.Request, response.Response, time.Since(ttStart))
|
||||
}
|
||||
}
|
||||
|
||||
func makeErrorStatus(reason string, args ...interface{}) *admissionv1beta1.AdmissionResponse {
|
||||
result := apierrors.NewBadRequest(fmt.Sprintf(reason, args...)).Status()
|
||||
return &admissionv1beta1.AdmissionResponse{
|
||||
|
|
|
@ -155,7 +155,7 @@ func NewDurableConnection(target string, messageChan chan []byte, logger *zap.Su
|
|||
select {
|
||||
case <-ticker.C:
|
||||
if err := c.write(websocket.PingMessage, []byte{}); err != nil {
|
||||
logger.Errorw("Failed to send ping message", zap.Error(err))
|
||||
logger.Errorw("Failed to send ping message to "+target, zap.Error(err))
|
||||
}
|
||||
case <-c.closeChan:
|
||||
return
|
||||
|
|
Loading…
Reference in New Issue