Auto-update dependencies (#93)

Produced via:
  `dep ensure -update knative.dev/test-infra knative.dev/pkg`
/assign mattmoor
This commit is contained in:
mattmoor-sockpuppet 2019-09-06 07:25:05 -07:00 committed by Knative Prow Robot
parent effd5c6430
commit e3145bc1cb
15 changed files with 446 additions and 341 deletions

6
Gopkg.lock generated
View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

@ -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

View File

@ -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"`

View File

@ -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

View 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>

View File

@ -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(`&emsp; &emsp; <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
}

View File

@ -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 }}
&emsp; &emsp; <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>

View File

@ -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)

View File

@ -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
}

View File

@ -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{

View File

@ -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