Auto-update dependencies (#68)

Produced via:
  `dep ensure -update knative.dev/test-infra knative.dev/pkg`
/assign @mattmoor
This commit is contained in:
mattmoor-sockpuppet 2019-08-07 07:22:56 -07:00 committed by Knative Prow Robot
parent 2c1a329543
commit 63dd62a744
27 changed files with 2325 additions and 11 deletions

6
Gopkg.lock generated
View File

@ -927,7 +927,7 @@
[[projects]]
branch = "master"
digest = "1:a54bc33aee102c303962336ef74981e9c49f4a4a26f411c5bd9f6b6b248fd3c3"
digest = "1:5e2eff8a4b0de901e3183489bcfa0a303ff249e74939b2259578ea23a093b5b8"
name = "knative.dev/pkg"
packages = [
"apis",
@ -946,7 +946,7 @@
"metrics/metricskey",
]
pruneopts = "T"
revision = "8a10634b4fa4df6dc450bfd883e85a33e76f2471"
revision = "30a47c2c75046e401d6f4fd9e95b523f71107229"
[[projects]]
branch = "master"
@ -957,7 +957,7 @@
"tools/dep-collector",
]
pruneopts = "UT"
revision = "d65201fadb82dbdc5713f9ba9a1313150c7aa9b5"
revision = "8356197d9783864a845a21dca3e898faa5d7d7c5"
[solve-meta]
analyzer-name = "dep"

12
vendor/knative.dev/pkg/Gopkg.lock generated vendored
View File

@ -911,7 +911,7 @@
version = "kubernetes-1.12.9"
[[projects]]
digest = "1:132ea9b0bdac4c461e2de359d44ece44531b3062c1d52ff88a7cc8857be25063"
digest = "1:476d1780b33b4c9a27e7f7e722f36115d1e1ccd0fca97be331181c84e0f9ae68"
name = "k8s.io/client-go"
packages = [
"discovery",
@ -1059,9 +1059,11 @@
"pkg/apis/clientauthentication/v1beta1",
"pkg/version",
"plugin/pkg/client/auth/exec",
"plugin/pkg/client/auth/gcp",
"rest",
"rest/watch",
"testing",
"third_party/forked/golang/template",
"tools/auth",
"tools/cache",
"tools/clientcmd",
@ -1079,6 +1081,7 @@
"util/flowcontrol",
"util/homedir",
"util/integer",
"util/jsonpath",
"util/retry",
"util/workqueue",
]
@ -1149,14 +1152,14 @@
[[projects]]
branch = "master"
digest = "1:68050b4ce26531655bac0f6e109d04eb0a4550ead73e320119ed3c628ced4a0a"
digest = "1:39bb1014c454fbf9d8f4eab62a44fcf8d3ade774e2b42015e533e5d9f3fe23d9"
name = "knative.dev/test-infra"
packages = [
"scripts",
"tools/dep-collector",
]
pruneopts = "UT"
revision = "17f2331e80ad0d3e170ea2bae45c3922744f83af"
revision = "d65201fadb82dbdc5713f9ba9a1313150c7aa9b5"
[solve-meta]
analyzer-name = "dep"
@ -1196,12 +1199,14 @@
"go.uber.org/zap/zapcore",
"go.uber.org/zap/zaptest",
"golang.org/x/sync/errgroup",
"gopkg.in/yaml.v2",
"k8s.io/api/admission/v1beta1",
"k8s.io/api/admissionregistration/v1beta1",
"k8s.io/api/apps/v1",
"k8s.io/api/authentication/v1",
"k8s.io/api/batch/v1",
"k8s.io/api/core/v1",
"k8s.io/api/extensions/v1beta1",
"k8s.io/api/rbac/v1",
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset",
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake",
@ -1242,6 +1247,7 @@
"k8s.io/client-go/kubernetes/fake",
"k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1",
"k8s.io/client-go/kubernetes/typed/core/v1",
"k8s.io/client-go/plugin/pkg/client/auth/gcp",
"k8s.io/client-go/rest",
"k8s.io/client-go/testing",
"k8s.io/client-go/tools/cache",

View File

@ -150,12 +150,12 @@ func WaitForEndpointStateWithTimeout(
defer logging.GetEmitableSpan(context.Background(), fmt.Sprintf("WaitForEndpointState/%s", desc)).End()
// Try parsing the "theURL" with and without a scheme.
asURL, err := url.Parse(fmt.Sprintf("http://%s", theURL))
asURL, err := url.Parse(theURL)
if err != nil {
asURL, err = url.Parse(theURL)
if err != nil {
return nil, err
}
return nil, err
}
if asURL.Scheme == "" {
asURL.Scheme = "http"
}
req, err := http.NewRequest(http.MethodGet, asURL.String(), nil)

View File

@ -0,0 +1,23 @@
# Coverage Calculator
`coveragecalculator` package contains types and helper methods pertaining to
coverage calculation.
[TypeCoverage](coveragedata.go) is a type to represent coverage data for a
particular API object type. This is the wire contract between the webhook server
running inside the K8s cluster and any client using the API-Coverage tool. All
API calls into the webhook-server would return response containing this object
to represent coverage data.
[IgnoredFields](ignorefields.go) type provides ability for individual repos to
specify fields that they would like the API Coverage tool to ignore for coverage
calculation. Individual repos are expected to provide a .yaml file providing
fields that they would like to ignore and use helper method
`ReadFromFile(filePath)` to read and intialize this type. `FieldIgnored()` can
then be called by providing `packageName`, `typeName` and `FieldName` to check
if the field needs to be ignored.
[CalculateCoverage](calculator.go) method provides a capability to calculate
coverage values. This method takes an array of [TypeCoverage](coveragedata.go)
and iterates over them to aggreage coverage values. The aggregate result is
encapsulated inside [CoverageValues](calculator.go) and returned.

View File

@ -0,0 +1,40 @@
/*
Copyright 2019 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.
*/
package coveragecalculator
// CoverageValues encapsulates all the coverage related values.
type CoverageValues struct {
TotalFields int
CoveredFields int
IgnoredFields int
}
// CalculateTypeCoverage calculates aggregate coverage values based on provided []TypeCoverage
func CalculateTypeCoverage(typeCoverage []TypeCoverage) *CoverageValues {
cv := CoverageValues{}
for _, coverage := range typeCoverage {
for _, field := range coverage.Fields {
cv.TotalFields++
if field.Ignored {
cv.IgnoredFields++
} else if field.Coverage {
cv.CoveredFields++
}
}
}
return &cv
}

View File

@ -0,0 +1,51 @@
/*
Copyright 2019 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.
*/
package coveragecalculator
import "k8s.io/apimachinery/pkg/util/sets"
// FieldCoverage represents coverage data for a field.
type FieldCoverage struct {
Field string `json:"Field"`
Values sets.String `json:"Values"`
Coverage bool `json:"Covered"`
Ignored bool `json:"Ignored"`
}
// Merge operation merges the field coverage data when multiple nodes represent the same type. (e.g. ConnectedNodes traversal)
func (f *FieldCoverage) Merge(coverage bool, values sets.String) {
if coverage {
f.Coverage = coverage
f.Values = f.Values.Union(values)
}
}
// GetValues returns Values as slice
func (f *FieldCoverage) GetValues() []string {
values := []string{}
for key := range f.Values {
values = append(values, key)
}
return values
}
// TypeCoverage encapsulates type information and field coverage.
type TypeCoverage struct {
Package string `json:"Package"`
Type string `json:"Type"`
Fields map[string]*FieldCoverage `json:"Fields"`
}

View File

@ -0,0 +1,81 @@
/*
Copyright 2019 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.
*/
package coveragecalculator
import (
"fmt"
"io/ioutil"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
yaml "gopkg.in/yaml.v2"
)
// IgnoredFields encapsulates fields to be ignored in a package for API coverage calculation.
type IgnoredFields struct {
ignoredFields map[string]sets.String
}
// This type is used for deserialization from the input .yaml file
type inputIgnoredFields struct {
Package string `yaml:"package"`
Type string `yaml:"type"`
Fields []string `yaml:"fields"`
}
// ReadFromFile is a utility method that can be used by repos to read .yaml input file into
// IgnoredFields type.
func (ig *IgnoredFields) ReadFromFile(filePath string) error {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("Error reading file: %s Error : %v", filePath, err)
}
var inputEntries []inputIgnoredFields
err = yaml.Unmarshal(data, &inputEntries)
if err != nil {
return fmt.Errorf("Error unmarshalling ignoredfields input yaml file: %s Content: %s Error: %v", filePath, string(data), err)
}
ig.ignoredFields = make(map[string]sets.String)
for _, entry := range inputEntries {
if _, ok := ig.ignoredFields[entry.Package]; !ok {
ig.ignoredFields[entry.Package] = sets.String{}
}
for _, field := range entry.Fields {
ig.ignoredFields[entry.Package].Insert(entry.Type + "." + field)
}
}
return nil
}
// FieldIgnored method given a package, type and field returns true if the field is marked ignored.
func (ig *IgnoredFields) FieldIgnored(packageName string, typeName string, fieldName string) bool {
if ig.ignoredFields != nil {
for key, value := range ig.ignoredFields {
if strings.HasSuffix(packageName, key) {
if value.Has(typeName + "." + fieldName) {
return true
}
}
}
}
return false
}

View File

@ -0,0 +1,60 @@
# Resource Tree
resourcetree package contains types and interfaces that define a resource
tree(n-ary tree based representation of an API resource). Each resource tree is
composed of nodes which have data encapsulated inside [nodeData](node.go) and
operations that can be performed on each node in the interface
[NodeInterface](node.go). Type of a node is logically defined by reflect.Kind.
Each node type is expected to satisfy the [NodeInterface](node.go) interface.
## Resource Forest
[ResourceForest](resourceforest.go) groups all resource trees that are part of
an API version into a single construct and defines operations that span across
them. As an example for Knative Serving, we will have individual resource trees
for Configuration, Revision, Route and Service, and they are encapsulated inside
a resource forest under version v1alpha1. Example of an operation that spans
resource trees would be to get coverage details for outlined types connected
using ConnectedNodes.
ConnectedNodes represent connections between nodes that are of same
type(reflect.Type) and belong to same package but span across different trees or
branches of same tree. An example of ConnectedNodes would be
v1alpha1.Route.Spec.Traffic and v1alpha1.Route.Status.Traffic, both these
Traffic fields are of type v1alpha1.TrafficTarget, but are present in different
paths inside the resource tree. ConnectedNodes connects these two nodes, and an
outlining of this type would present the coverage across the two branches and
gives a unified view of what fields are covered.
## Type Analysis
A Resource tree is built using reflect.Type Each node type is expected to
implement NodeInterface method _buildChildNodes(t reflect.Type)_. Inside this
method each node creates child nodes based on its type, for e.g. StructKindNode
creates one child for each field defined in the struct. Type analysis are
defined inside [typeanalyzer_tests](buildChildNodes_test.go)
## Value Analysis
A Resource tree is updated using reflect.Value Each node type is expected to
implement NodeInterface method _updateCoverage(v reflect.Value)_. Inisde this
method each node updates its nodeData.covered field based on whether the
reflect.Value parameter being passed is set or not.
## Rules
To define traversal pattern on a [resourcetree](../resourcetree/resourcetree.go)
`coveragecalculator` package supports defining rules that are applied during
apicoverage walk-through. Currently the tool supports two types of Rule objects:
1. `NodeRules`: Enforces node level semantics. e.g.: Avoid traversing any type
that belong to a particular package. To enforce this rule, the repo would
define an object that implements the [NodeRule](rule.go) interface's
`Apply(nodeInterface resourcetree.NodeInterface) bool` method and pass that
onto the `resourcetree` traversal routine.
1. `FieldRules`: Enforces field level semantics. e.g. Avoid calculating coverage
for any field that starts with prefix `deprecated`. To enforce this rule, the
repo would define an object that implements [FieldRule](rule.go) interface's
`Apply(fieldName string) bool` method and pass that onto the `resourcetree`
traversal routine.

View File

@ -0,0 +1,71 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
const (
arrayNodeNameSuffix = "-arr"
)
// ArrayKindNode represents resource tree node of types reflect.Kind.Array and reflect.Kind.Slice
type ArrayKindNode struct {
NodeData
// Array type e.g. []int will store reflect.Kind.Int.
// This is required for type-expansion and value-evaluation decisions.
arrKind reflect.Kind
}
// GetData returns node data
func (a *ArrayKindNode) GetData() NodeData {
return a.NodeData
}
func (a *ArrayKindNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
a.NodeData.initialize(field, parent, t, rt)
a.arrKind = t.Elem().Kind()
}
func (a *ArrayKindNode) buildChildNodes(t reflect.Type) {
childName := a.Field + arrayNodeNameSuffix
childNode := a.Tree.createNode(childName, a, t.Elem())
a.Children[childName] = childNode
childNode.buildChildNodes(t.Elem())
}
func (a *ArrayKindNode) updateCoverage(v reflect.Value) {
if !v.IsNil() {
a.Covered = true
for i := 0; i < v.Len(); i++ {
a.Children[a.Field+arrayNodeNameSuffix].updateCoverage(v.Index(i))
}
}
}
func (a *ArrayKindNode) buildCoverageData(coverageHelper coverageDataHelper) {
if a.arrKind == reflect.Struct {
a.Children[a.Field+arrayNodeNameSuffix].buildCoverageData(coverageHelper)
}
}
func (a *ArrayKindNode) getValues() sets.String {
return nil
}

View File

@ -0,0 +1,95 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"fmt"
"reflect"
"strconv"
"k8s.io/apimachinery/pkg/util/sets"
)
// BasicTypeKindNode represents resource tree node of basic types like int, float, etc.
type BasicTypeKindNode struct {
NodeData
values sets.String // Values seen for this node. Useful for enum types.
possibleEnum bool // Flag to indicate if this is a possible enum.
}
// GetData returns node data
func (b *BasicTypeKindNode) GetData() NodeData {
return b.NodeData
}
func (b *BasicTypeKindNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
b.NodeData.initialize(field, parent, t, rt)
b.values = sets.String{}
b.NodeData.LeafNode = true
}
func (b *BasicTypeKindNode) buildChildNodes(t reflect.Type) {
// Treating bools as possible enums to support tighter coverage information.
if t.Name() != t.Kind().String() || b.FieldType.Kind() == reflect.Bool {
b.possibleEnum = true
}
}
func (b *BasicTypeKindNode) updateCoverage(v reflect.Value) {
if value := b.string(v); len(value) > 0 {
if b.possibleEnum || b.FieldType.Kind() == reflect.Bool {
b.values.Insert(value)
}
b.Covered = true
}
}
// no-op as the coverage is calculated as field coverage in parent node.
func (b *BasicTypeKindNode) buildCoverageData(coverageHelper coverageDataHelper) {}
func (b *BasicTypeKindNode) string(v reflect.Value) string {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() != 0 {
return strconv.Itoa(int(v.Int()))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() != 0 {
return strconv.FormatUint(v.Uint(), 10)
}
case reflect.Float32, reflect.Float64:
if v.Float() != 0 {
return fmt.Sprintf("%f", v.Float())
}
case reflect.String:
if v.Len() != 0 {
return v.String()
}
case reflect.Bool:
return strconv.FormatBool(v.Bool())
}
return ""
}
func (b *BasicTypeKindNode) getValues() sets.String {
if b.possibleEnum {
return b.values
}
return nil
}

View File

@ -0,0 +1,69 @@
/*
Copyright 2019 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.
*/
// node.go contains types and interfaces pertaining to nodes inside resource tree.
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
// NodeInterface defines methods that can be performed on each node in the resource tree.
type NodeInterface interface {
GetData() NodeData
initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree)
buildChildNodes(t reflect.Type)
updateCoverage(v reflect.Value)
buildCoverageData(coverageDataHelper coverageDataHelper)
getValues() sets.String
}
// NodeData is the data stored in each node of the resource tree.
type NodeData struct {
// Represents the Name of the field e.g. field name inside the struct.
Field string
// Reference back to the resource tree. Required for cross-tree traversal(connected nodes traversal)
Tree *ResourceTree
// Required as type information is not available during tree traversal.
FieldType reflect.Type
// Path in the resource tree reaching this node.
NodePath string
// Link back to parent.
Parent NodeInterface
// Child nodes are keyed using field names(nodeData.field).
Children map[string]NodeInterface
// Storing this as an additional field because type-analysis determines the value,
// which gets used later in value-evaluation
LeafNode bool
Covered bool
}
func (nd *NodeData) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
nd.Field = field
nd.Tree = rt
nd.Parent = parent
nd.FieldType = t
nd.Children = make(map[string]NodeInterface)
if parent != nil {
nd.NodePath = parent.GetData().NodePath + "." + field
} else {
nd.NodePath = field
}
}

View File

@ -0,0 +1,53 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
// OtherKindNode represents nodes in the resource tree of types like maps, interfaces, etc
type OtherKindNode struct {
NodeData
}
// GetData returns node data
func (o *OtherKindNode) GetData() NodeData {
return o.NodeData
}
func (o *OtherKindNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
o.NodeData.initialize(field, parent, t, rt)
o.NodeData.LeafNode = true
}
func (o *OtherKindNode) buildChildNodes(t reflect.Type) {}
func (o *OtherKindNode) updateCoverage(v reflect.Value) {
if !v.IsNil() {
o.Covered = true
}
}
// no-op as the coverage is calculated as field coverage in parent node.
func (o *OtherKindNode) buildCoverageData(coverageHelper coverageDataHelper) {}
func (o *OtherKindNode) getValues() sets.String {
return nil
}

View File

@ -0,0 +1,67 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
const (
ptrNodeNameSuffix = "-ptr"
)
// PtrKindNode represents nodes in the resource tree of type reflect.Kind.Ptr, reflect.Kind.UnsafePointer, etc.
type PtrKindNode struct {
NodeData
objKind reflect.Kind // Type of the object being pointed to. Eg: *int will store reflect.Kind.Int. This is required for type-expansion and value-evaluation decisions.
}
// GetData returns node data
func (p *PtrKindNode) GetData() NodeData {
return p.NodeData
}
func (p *PtrKindNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
p.NodeData.initialize(field, parent, t, rt)
p.objKind = t.Elem().Kind()
}
func (p *PtrKindNode) buildChildNodes(t reflect.Type) {
childName := p.Field + ptrNodeNameSuffix
childNode := p.Tree.createNode(childName, p, t.Elem())
p.Children[childName] = childNode
childNode.buildChildNodes(t.Elem())
}
func (p *PtrKindNode) updateCoverage(v reflect.Value) {
if !v.IsNil() {
p.Covered = true
p.Children[p.Field+ptrNodeNameSuffix].updateCoverage(v.Elem())
}
}
func (p *PtrKindNode) buildCoverageData(coverageHelper coverageDataHelper) {
if p.objKind == reflect.Struct {
p.Children[p.Field+ptrNodeNameSuffix].buildCoverageData(coverageHelper)
}
}
func (p *PtrKindNode) getValues() sets.String {
return nil
}

View File

@ -0,0 +1,80 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"container/list"
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
)
// ResourceForest represents the top-level forest that contains individual resource trees
// for top-level resource types and all connected nodes across resource trees.
type ResourceForest struct {
Version string
// Key is ResourceTree.ResourceName
TopLevelTrees map[string]ResourceTree
// Head of the linked list keyed by nodeData.fieldType.pkg + nodeData.fieldType.Name()
ConnectedNodes map[string]*list.List
}
// AddResourceTree adds a resource tree to the resource forest.
func (r *ResourceForest) AddResourceTree(resourceName string, resourceType reflect.Type) {
tree := ResourceTree{
ResourceName: resourceName,
Forest: r,
}
tree.BuildResourceTree(resourceType)
r.TopLevelTrees[resourceName] = tree
}
// getConnectedNodeCoverage calculates the outlined coverage for a Type using ConnectedNodes linkedlist.
// We traverse through each element in the linkedlist and merge
// coverage data into a single coveragecalculator.TypeCoverage object.
func (r *ResourceForest) getConnectedNodeCoverage(
fieldType reflect.Type,
fieldRules FieldRules,
ignoredFields coveragecalculator.IgnoredFields) coveragecalculator.TypeCoverage {
packageName := fieldType.PkgPath()
coverage := coveragecalculator.TypeCoverage{
Type: fieldType.Name(),
Package: packageName,
Fields: make(map[string]*coveragecalculator.FieldCoverage),
}
if value, ok := r.ConnectedNodes[fieldType.PkgPath()+"."+fieldType.Name()]; ok {
for elem := value.Front(); elem != nil; elem = elem.Next() {
node := elem.Value.(NodeInterface)
for field, v := range node.GetData().Children {
if fieldRules.Apply(field) {
if _, ok := coverage.Fields[field]; !ok {
coverage.Fields[field] = &coveragecalculator.FieldCoverage{
Field: field,
Ignored: ignoredFields.FieldIgnored(packageName, fieldType.Name(), field),
Values: sets.String{},
}
}
// merge values across the list.
coverage.Fields[field].Merge(v.GetData().Covered, v.getValues())
}
}
}
}
return coverage
}

View File

@ -0,0 +1,97 @@
/*
Copyright 2019 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.
*/
package resourcetree
import (
"container/list"
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
)
// ResourceTree encapsulates a tree corresponding to a resource type.
type ResourceTree struct {
ResourceName string
Root NodeInterface
Forest *ResourceForest
}
// coverageDataHelper is a encapsulator parameter type to the BuildCoverageData method
// so as to avoid long parameter list.
type coverageDataHelper struct {
typeCoverage *[]coveragecalculator.TypeCoverage
nodeRules NodeRules
fieldRules FieldRules
ignoredFields coveragecalculator.IgnoredFields
coveredTypes sets.String
}
func (r *ResourceTree) createNode(field string, parent NodeInterface, t reflect.Type) NodeInterface {
var n NodeInterface
switch t.Kind() {
case reflect.Struct:
n = new(StructKindNode)
case reflect.Array, reflect.Slice:
n = new(ArrayKindNode)
case reflect.Ptr, reflect.UnsafePointer, reflect.Uintptr:
n = new(PtrKindNode)
case reflect.Bool, reflect.String, reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint,
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
n = new(BasicTypeKindNode)
default:
n = new(OtherKindNode) // Maps, interfaces, etc
}
n.initialize(field, parent, t, r)
if len(t.PkgPath()) != 0 {
typeName := t.PkgPath() + "." + t.Name()
if _, ok := r.Forest.ConnectedNodes[typeName]; !ok {
r.Forest.ConnectedNodes[typeName] = list.New()
}
r.Forest.ConnectedNodes[typeName].PushBack(n)
}
return n
}
// BuildResourceTree builds a resource tree by calling into analyzeType method starting from root.
func (r *ResourceTree) BuildResourceTree(t reflect.Type) {
r.Root = r.createNode(r.ResourceName, nil, t)
r.Root.buildChildNodes(t)
}
// UpdateCoverage updates coverage data in the resource tree based on the provided reflect.Value
func (r *ResourceTree) UpdateCoverage(v reflect.Value) {
r.Root.updateCoverage(v)
}
// BuildCoverageData calculates the coverage information for a resource tree by applying provided Node and Field rules.
func (r *ResourceTree) BuildCoverageData(nodeRules NodeRules, fieldRules FieldRules,
ignoredFields coveragecalculator.IgnoredFields) []coveragecalculator.TypeCoverage {
coverageHelper := coverageDataHelper{
nodeRules: nodeRules,
fieldRules: fieldRules,
typeCoverage: &[]coveragecalculator.TypeCoverage{},
ignoredFields: ignoredFields,
coveredTypes: sets.String{},
}
r.Root.buildCoverageData(coverageHelper)
return *coverageHelper.typeCoverage
}

View File

@ -0,0 +1,49 @@
/*
Copyright 2019 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.
*/
package resourcetree
// rule.go contains different rules that can be defined to control resource tree traversal.
// NodeRules encapsulates all the node level rules defined by a repo.
type NodeRules struct {
Rules []func(nodeInterface NodeInterface) bool
}
// Apply runs all the rules defined by a repo against a node.
func (n *NodeRules) Apply(node NodeInterface) bool {
for _, rule := range n.Rules {
if !rule(node) {
return false
}
}
return true
}
// FieldRules encapsulates all the field level rules defined by a repo.
type FieldRules struct {
Rules []func(fieldName string) bool
}
// Apply runs all the rules defined by a repo against a field.
func (f *FieldRules) Apply(fieldName string) bool {
for _, rule := range f.Rules {
if !rule(fieldName) {
return false
}
}
return true
}

View File

@ -0,0 +1,109 @@
/*
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.
*/
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
const (
v1TimeType = "v1.Time"
volatileTimeType = "apis.VolatileTime"
)
// StructKindNode represents nodes in the resource tree of type reflect.Kind.Struct
type StructKindNode struct {
NodeData
}
// GetData returns node data
func (s *StructKindNode) GetData() NodeData {
return s.NodeData
}
func (s *StructKindNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
s.NodeData.initialize(field, parent, t, rt)
}
func (s *StructKindNode) buildChildNodes(t reflect.Type) {
// For types that are part of the standard package, we treat them as leaf nodes and don't expand further.
// https://golang.org/pkg/reflect/#StructField.
if len(s.FieldType.PkgPath()) == 0 {
s.LeafNode = true
return
}
for i := 0; i < t.NumField(); i++ {
var childNode NodeInterface
if s.isTimeNode(t.Field(i).Type) {
childNode = new(TimeTypeNode)
childNode.initialize(t.Field(i).Name, s, t.Field(i).Type, s.Tree)
} else {
childNode = s.Tree.createNode(t.Field(i).Name, s, t.Field(i).Type)
}
s.Children[t.Field(i).Name] = childNode
childNode.buildChildNodes(t.Field(i).Type)
}
}
func (s *StructKindNode) isTimeNode(t reflect.Type) bool {
if t.Kind() == reflect.Struct {
return t.String() == v1TimeType || t.String() == volatileTimeType
} else if t.Kind() == reflect.Ptr {
return t.Elem().String() == v1TimeType || t.String() == volatileTimeType
} else {
return false
}
}
func (s *StructKindNode) updateCoverage(v reflect.Value) {
if v.IsValid() {
s.Covered = true
if !s.LeafNode {
for i := 0; i < v.NumField(); i++ {
s.Children[v.Type().Field(i).Name].updateCoverage(v.Field(i))
}
}
}
}
func (s *StructKindNode) buildCoverageData(coverageHelper coverageDataHelper) {
if len(s.Children) == 0 {
return
}
coverage := s.Tree.Forest.getConnectedNodeCoverage(s.FieldType, coverageHelper.fieldRules, coverageHelper.ignoredFields)
*coverageHelper.typeCoverage = append(*coverageHelper.typeCoverage, coverage)
// Adding the type to covered fields so as to avoid revisiting the same node in other parts of the resource tree.
coverageHelper.coveredTypes.Insert(s.FieldType.PkgPath() + "." + s.FieldType.Name())
for field := range coverage.Fields {
node := s.Children[field]
if !coverage.Fields[field].Ignored && node.GetData().Covered && coverageHelper.nodeRules.Apply(node) {
// Check to see if the type has already been covered.
if !coverageHelper.coveredTypes.Has(node.GetData().FieldType.PkgPath() + "." + node.GetData().FieldType.Name()) {
node.buildCoverageData(coverageHelper)
}
}
}
}
func (s *StructKindNode) getValues() sets.String {
return nil
}

View File

@ -0,0 +1,399 @@
/*
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.
*/
package resourcetree
//test_util contains types defined and used by types and their corresponding verification methods.
import (
"container/list"
"errors"
"fmt"
"reflect"
)
const (
basicTypeName = "BasicType"
ptrTypeName = "PtrType"
arrayTypeName = "ArrayType"
otherTypeName = "OtherType"
combinedTypeName = "CombinedType"
)
type baseType struct {
field1 string
field2 int16
}
type ptrType struct {
structPtr *baseType
basePtr *float32
}
type arrayType struct {
structArr []baseType
baseArr []bool
}
type otherType struct {
structMap map[string]baseType
baseMap map[string]string
}
type combinedNodeType struct {
b baseType
a arrayType
p ptrType
}
func getBaseTypeValue() baseType {
return baseType{
field1: "test",
}
}
func getPtrTypeValueAllCovered() ptrType {
f := new(float32)
*f = 3.142
b := getBaseTypeValue()
return ptrType{
basePtr: f,
structPtr: &b,
}
}
func getPtrTypeValueSomeCovered() ptrType {
b := getBaseTypeValue()
return ptrType{
structPtr: &b,
}
}
func getArrValueAllCovered() arrayType {
b1 := getBaseTypeValue()
b2 := baseType{
field2: 32,
}
return arrayType{
structArr: []baseType{b1, b2},
baseArr: []bool{true, false},
}
}
func getArrValueSomeCovered() arrayType {
return arrayType{
structArr: []baseType{getBaseTypeValue()},
}
}
func getOtherTypeValue() otherType {
m := make(map[string]baseType)
m["test"] = getBaseTypeValue()
return otherType{
structMap: m,
}
}
func getTestTree(treeName string, t reflect.Type) *ResourceTree {
forest := ResourceForest{
Version: "TestVersion",
ConnectedNodes: make(map[string]*list.List),
TopLevelTrees: make(map[string]ResourceTree),
}
tree := ResourceTree{
ResourceName: treeName,
Forest: &forest,
}
tree.BuildResourceTree(t)
forest.TopLevelTrees[treeName] = tree
return &tree
}
func verifyBaseTypeNode(logPrefix string, data NodeData) error {
if len(data.Children) != 2 {
return fmt.Errorf("%s Expected 2 Children got only : %d", logPrefix, len(data.Children))
}
if value, ok := data.Children["field1"]; ok {
n := value.GetData()
if !n.LeafNode || n.FieldType.Kind() != reflect.String || n.FieldType.PkgPath() != "" || len(n.Children) != 0 {
return fmt.Errorf("%s Unexpected field: field1. Expected LeafNode:true, Kind: %s, pkgPath: '' Children: 0 Found LeafNode: %t Kind: %s pkgPath: %s Children:%d",
logPrefix, reflect.String, n.LeafNode, n.FieldType.Kind(), n.FieldType.PkgPath(), len(n.Children))
}
} else {
return fmt.Errorf("%s field1 child Not found", logPrefix)
}
return nil
}
func verifyPtrNode(data NodeData) error {
if len(data.Children) != 2 {
return fmt.Errorf("Expected 2 Children got: %d", len(data.Children))
}
child := data.Children["structPtr"]
if len(child.GetData().Children) != 1 {
return fmt.Errorf("Unexpected size for field:structPtr. Expected : 1, Found : %d", len(child.GetData().Children))
}
child = child.GetData().Children["structPtr-ptr"]
if err := verifyBaseTypeNode("child structPtr-ptr: ", child.GetData()); err != nil {
return err
}
child = data.Children["basePtr"]
if len(child.GetData().Children) != 1 {
return fmt.Errorf("Unexpected size for field:basePtr. Expected : 1 Found : %d", len(child.GetData().Children))
}
child = child.GetData().Children["basePtr-ptr"]
d := child.GetData()
if d.FieldType.Kind() != reflect.Float32 || !d.LeafNode || d.FieldType.PkgPath() != "" || len(d.Children) != 0 {
return fmt.Errorf("Unexpected field:basePtr-ptr: Expected: Kind: %s, LeafNode: true, pkgPath: '' Children: 0 Found Kind: %s, LeafNode: %t, pkgPath: %s Children:%d",
reflect.Float32, d.FieldType.Kind(), d.LeafNode, d.FieldType.PkgPath(), len(d.Children))
}
return nil
}
func verifyArrayNode(data NodeData) error {
if len(data.Children) != 2 {
return fmt.Errorf("Expected 2 Children got: %d", len(data.Children))
}
child := data.Children["structArr"]
d := child.GetData()
if d.FieldType.Kind() != reflect.Slice {
return fmt.Errorf("Unexpected kind for field:structArr: Expected : %s Found: %s", reflect.Slice, d.FieldType.Kind())
} else if len(d.Children) != 1 {
return fmt.Errorf("Unexpected number of Children for field:structArr: Expected : 1 Found : %d", len(d.Children))
}
child = child.GetData().Children["structArr-arr"]
if err := verifyBaseTypeNode("child structArr-arr:", child.GetData()); err != nil {
return err
}
child = data.Children["baseArr"]
d = child.GetData()
if d.FieldType.Kind() != reflect.Slice {
return fmt.Errorf("Unexpected kind for field:baseArr: Expected : %s Found : %s", reflect.Slice, d.FieldType.Kind())
} else if len(d.Children) != 1 {
return fmt.Errorf("Unexpected number of Children for field:baseArr: Expected : 1 Found : %d", len(d.Children))
}
child = child.GetData().Children["baseArr-arr"]
d = child.GetData()
if d.FieldType.Kind() != reflect.Bool || !d.LeafNode || d.FieldType.PkgPath() != "" || len(d.Children) != 0 {
return fmt.Errorf("Unexpected field:baseArr-arr Expected kind: %s, LeafNode: true, pkgPath: '', Children:0 Found: kind: %s, LeafNode: %t, pkgPath: %s, Children:%d",
reflect.Bool, d.FieldType.Kind(), d.LeafNode, d.FieldType.PkgPath(), len(d.Children))
}
return nil
}
func verifyOtherTypeNode(data NodeData) error {
if len(data.Children) != 2 {
return fmt.Errorf("OtherTypeVerification: Expected 2 Children got: %d", len(data.Children))
}
child := data.Children["structMap"]
d := child.GetData()
if d.FieldType.Kind() != reflect.Map || !d.LeafNode || len(d.Children) != 0 {
return fmt.Errorf("Unexpected field:structMap - Expected Kind: %s, LeafNode: true, Children:0 Found Kind: %s, LeafNode: %t, Children: %d",
reflect.Map, d.FieldType.Kind(), d.LeafNode, len(d.Children))
}
child = data.Children["baseMap"]
d = child.GetData()
if d.FieldType.Kind() != reflect.Map || !d.LeafNode || len(d.Children) != 0 {
return fmt.Errorf("Unexpected field:structMap - Expected Kind: %s, LeafNode: true, Children: 0 Found kind: %s, LeafNode: %t, Children: %d",
reflect.Map, d.FieldType.Kind(), d.LeafNode, len(d.Children))
}
return nil
}
func verifyResourceForest(forest *ResourceForest) error {
if len(forest.ConnectedNodes) != 4 {
return fmt.Errorf("Invalid number of connected nodes found. Expected : 4, Found : %d", len(forest.ConnectedNodes))
}
baseType := reflect.TypeOf(baseType{})
if value, found := forest.ConnectedNodes[baseType.PkgPath()+"."+baseType.Name()]; !found {
return errors.New("Cannot find baseType{} connectedNode")
} else if value.Len() != 3 {
return fmt.Errorf("Invalid length of baseType{} Node. Expected : 3 Found : %d", value.Len())
}
arrayType := reflect.TypeOf(arrayType{})
if value, found := forest.ConnectedNodes[arrayType.PkgPath()+"."+arrayType.Name()]; !found {
return errors.New("Cannot find arrayType{} connectedNode")
} else if value.Len() != 1 {
return fmt.Errorf("Invalid length of arrayType{} Node. Expected : 1 Found : %d", value.Len())
}
return nil
}
func verifyBaseTypeValue(logPrefix string, node NodeInterface) error {
if !node.GetData().Covered {
return errors.New(logPrefix + " Node marked as not-Covered. Expected to be Covered")
}
if !node.GetData().Children["field1"].GetData().Covered {
return errors.New(logPrefix + " field1 marked as not-Covered. Expected to be Covered")
}
if node.GetData().Children["field2"].GetData().Covered {
return errors.New(logPrefix + " field2 marked as Covered. Expected to be not-Covered")
}
return nil
}
func verifyPtrValueAllCovered(node NodeInterface) error {
if !node.GetData().Covered {
return errors.New("Node marked as not-Covered. Expected to be Covered")
}
child := node.GetData().Children["basePtr"]
if !child.GetData().Covered {
return errors.New("field:base_ptr marked as not-Covered. Expected to be Covered")
}
if !child.GetData().Children["basePtr"+ptrNodeNameSuffix].GetData().Covered {
return errors.New("field:basePtr" + ptrNodeNameSuffix + "marked as not-Covered. Expected to be Covered")
}
child = node.GetData().Children["structPtr"]
if !child.GetData().Covered {
return errors.New("field:structPtr marked as not-Covered. Expected to be Covered")
}
if err := verifyBaseTypeValue("field:structPtr"+ptrNodeNameSuffix, child.GetData().Children["structPtr"+ptrNodeNameSuffix]); err != nil {
return err
}
return nil
}
func verifyPtrValueSomeCovered(node NodeInterface) error {
if !node.GetData().Covered {
return errors.New("Node marked as not-Covered. Expected to be Covered")
}
child := node.GetData().Children["basePtr"]
if child.GetData().Covered {
return errors.New("field:basePtr marked as Covered. Expected to be not-Covered")
}
if child.GetData().Children["basePtr"+ptrNodeNameSuffix].GetData().Covered {
return errors.New("field:basePtr" + ptrNodeNameSuffix + "marked as Covered. Expected to be not-Covered")
}
child = node.GetData().Children["structPtr"]
if !child.GetData().Covered {
return errors.New("field:structPtr marked as not-Covered. Expected to be Covered")
}
if err := verifyBaseTypeValue("field:structPtr"+ptrNodeNameSuffix, child.GetData().Children["structPtr"+ptrNodeNameSuffix]); err != nil {
return err
}
return nil
}
func verifyArryValueAllCovered(node NodeInterface) error {
if !node.GetData().Covered {
return errors.New("Node marked as not-Covered. Expected to be Covered")
}
child := node.GetData().Children["baseArr"]
if !child.GetData().Covered {
return errors.New("field:baseArr marked as not-Covered. Expected to be Covered")
}
if !child.GetData().Children["baseArr"+arrayNodeNameSuffix].GetData().Covered {
return errors.New("field:baseArr" + arrayNodeNameSuffix + " marked as not-Covered. Expected to be Covered")
}
child = node.GetData().Children["structArr"]
if !child.GetData().Covered {
return errors.New("field:structArr marked as not-Covered. Expected to be Covered")
}
child = child.GetData().Children["structArr"+arrayNodeNameSuffix]
if !child.GetData().Covered {
return errors.New("structArr" + arrayNodeNameSuffix + " marked as not-Covered. Expected to be Covered")
}
if !child.GetData().Children["field1"].GetData().Covered {
return errors.New("structArr" + arrayNodeNameSuffix + ".field1 marked as not-Covered. Expected to be Covered")
}
if !child.GetData().Children["field2"].GetData().Covered {
return errors.New("structArr" + arrayNodeNameSuffix + ".field1 marked as not-Covered. Expected to be Covered")
}
return nil
}
func verifyArrValueSomeCovered(node NodeInterface) error {
if !node.GetData().Covered {
return errors.New("Node marked as not-Covered. Expected to be Covered")
}
child := node.GetData().Children["baseArr"]
if child.GetData().Covered {
return errors.New("field:baseArr marked as Covered. Expected to be not-Covered")
}
if child.GetData().Children["baseArr"+arrayNodeNameSuffix].GetData().Covered {
return errors.New("field:baseArr" + arrayNodeNameSuffix + " marked as Covered. Expected to be not-Covered")
}
child = node.GetData().Children["structArr"]
if !child.GetData().Covered {
return errors.New("field:structArr marked as not-Covered. Expected to be Covered")
}
if err := verifyBaseTypeValue("field:structArr"+arrayNodeNameSuffix, child.GetData().Children["structArr"+arrayNodeNameSuffix]); err != nil {
return err
}
return nil
}
func verifyOtherTypeValue(node NodeInterface) error {
if !node.GetData().Covered {
return errors.New("Node marked as not-Covered. Expected to be Covered")
}
if !node.GetData().Children["structMap"].GetData().Covered {
return errors.New("field:structMap marked as not-Covered. Expected to be Covered")
}
if node.GetData().Children["baseMap"].GetData().Covered {
return errors.New("field:baseMap marked as Covered. Expected to be not-Covered")
}
return nil
}

View File

@ -0,0 +1,57 @@
/*
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.
*/
package resourcetree
import (
"reflect"
"k8s.io/apimachinery/pkg/util/sets"
)
// TimeTypeNode is a node type that encapsulates fields that are internally time based. E.g metav1.ObjectMeta.CreationTimestamp or metav1.ObjectMeta.DeletionTimestamp.
// These are internally of type metav1.Time which use standard time type, but their values are specified as timestamp strings with parsing logic to create time objects. For
// use-case we only care if the value is set, so we create TimeTypeNodes and mark them as leafnodes.
type TimeTypeNode struct {
NodeData
}
// GetData returns node data
func (ti *TimeTypeNode) GetData() NodeData {
return ti.NodeData
}
func (ti *TimeTypeNode) initialize(field string, parent NodeInterface, t reflect.Type, rt *ResourceTree) {
ti.NodeData.initialize(field, parent, t, rt)
ti.LeafNode = true
}
func (ti *TimeTypeNode) buildChildNodes(t reflect.Type) {}
func (ti *TimeTypeNode) updateCoverage(v reflect.Value) {
if v.Type().Kind() == reflect.Struct && v.IsValid() {
ti.Covered = true
} else if v.Type().Kind() == reflect.Ptr && !v.IsNil() {
ti.Covered = true
}
}
// no-op as the coverage is calculated as field coverage in parent node.
func (ti *TimeTypeNode) buildCoverageData(coverageHelper coverageDataHelper) {}
func (ti *TimeTypeNode) getValues() sets.String {
return nil
}

View File

@ -0,0 +1,21 @@
# Tools
Tools package is intended to contain types and public helper methods that
provide utilities to solves common requirements across repos. It currently
contains following helper methods:
1. `GetDefaultKubePath`: Helper method to retrieve the path for Kubeconfig
inside users home directory.
1. `GetWebhookServiceIP`: Helper method to retrieve the public IP address for
the webhook service. The service is setup as part of the apicoverage-webhook
setup.
1. `GetResourceCoverage`: Helper method to retrieve Coverage data for a resource
passed as parameter. The coverage data is retrieved from the API that is
exposed by the HTTP server in [Webhook Setup](../webhook/webhook.go)
1. `GetAndWriteResourceCoverage`: Helper method that uses `GetResourceCoverage`
to retrieve resource coverage and writes output to a file.
1. `GetTotalCoverage`: Helper method to retrieve total coverage data for a repo.
The coverage data is retrieved from the API that is exposed by the HTTP
server in [Webhook Setup](../webhook/webhook.go)
1. `GetAndWriteTotalCoverage`: Helper method that uses `GetTotalCoverage` to
retrieve total coverage and writes output to a file.

View File

@ -0,0 +1,190 @@
/*
Copyright 2019 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.
*/
package tools
import (
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os/user"
"path"
"strings"
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
"knative.dev/pkg/test/webhook-apicoverage/view"
"knative.dev/pkg/test/webhook-apicoverage/webhook"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
// Mysteriously required to support GCP auth (required by k8s libs).
// Apparently just importing it is enough. @_@ side effects @_@.
// https://github.com/kubernetes/client-go/issues/242
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/tools/clientcmd"
)
// tools.go contains utility methods to help repos use the webhook-apicoverage tool.
const (
// WebhookResourceCoverageEndPoint constant for resource coverage API endpoint.
WebhookResourceCoverageEndPoint = "https://%s:443" + webhook.ResourceCoverageEndPoint + "?resource=%s"
// WebhookTotalCoverageEndPoint constant for total coverage API endpoint.
WebhookTotalCoverageEndPoint = "https://%s:443" + webhook.TotalCoverageEndPoint
)
// GetDefaultKubePath helper method to fetch kubeconfig path.
func GetDefaultKubePath() (string, error) {
var (
usr *user.User
err error
)
if usr, err = user.Current(); err != nil {
return "", fmt.Errorf("error retrieving current user: %v", err)
}
return path.Join(usr.HomeDir, ".kube/config"), nil
}
func getKubeClient(kubeConfigPath string, clusterName string) (*kubernetes.Clientset, error) {
overrides := clientcmd.ConfigOverrides{}
overrides.Context.Cluster = clusterName
clientCfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath},
&overrides).ClientConfig()
if err != nil {
return nil, fmt.Errorf("error building kube client config: %v", err)
}
var kubeClient *kubernetes.Clientset
if kubeClient, err = kubernetes.NewForConfig(clientCfg); err != nil {
return nil, fmt.Errorf("error building KubeClient from config: %v", err)
}
return kubeClient, nil
}
// GetWebhookServiceIP is a helper method to fetch IP Address of the LoadBalancer webhook service.
func GetWebhookServiceIP(kubeConfigPath string, clusterName string, namespace string, serviceName string) (string, error) {
kubeClient, err := getKubeClient(kubeConfigPath, clusterName)
if err != nil {
return "", err
}
svc, err := kubeClient.CoreV1().Services(namespace).Get(serviceName, v1.GetOptions{})
if err != nil {
return "", fmt.Errorf("error encountered while retrieving service: %s Error: %v", serviceName, err)
}
if len(svc.Status.LoadBalancer.Ingress) == 0 {
return "", fmt.Errorf("found zero Ingress instances for service: %s", serviceName)
}
return svc.Status.LoadBalancer.Ingress[0].IP, nil
}
// GetResourceCoverage is a helper method to get Coverage data for a resource from the service webhook.
func GetResourceCoverage(webhookIP string, resourceName string) (string, error) {
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Get(fmt.Sprintf(WebhookResourceCoverageEndPoint, webhookIP, resourceName))
if err != nil {
return "", fmt.Errorf("encountered error making resource coverage request: %v", err)
} else if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("invalid HTTP Status received for resource coverage request. Status: %d", resp.StatusCode)
}
var body []byte
if body, err = ioutil.ReadAll(resp.Body); err != nil {
return "", fmt.Errorf("error reading resource coverage response: %v", err)
}
return string(body), nil
}
// GetAndWriteResourceCoverage is a helper method that uses GetResourceCoverage to get coverage and write it to a file.
func GetAndWriteResourceCoverage(webhookIP string, resourceName string, outputFile string, displayRules view.DisplayRules) error {
var (
err error
resourceCoverage string
)
if resourceCoverage, err = GetResourceCoverage(webhookIP, resourceName); err != nil {
return err
}
if err = ioutil.WriteFile(outputFile, []byte(resourceCoverage), 0400); err != nil {
return fmt.Errorf("error writing resource coverage to output file: %s, error: %v coverage: %s", outputFile, err, resourceCoverage)
}
return nil
}
// GetTotalCoverage calls the total coverage API to retrieve total coverage values.
func GetTotalCoverage(webhookIP string) (*coveragecalculator.CoverageValues, error) {
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Get(fmt.Sprintf(WebhookTotalCoverageEndPoint, webhookIP))
if err != nil {
return nil, fmt.Errorf("encountered error making total coverage request: %v", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("invalid HTTP Status received for total coverage request. Status: %d", resp.StatusCode)
}
var body []byte
if body, err = ioutil.ReadAll(resp.Body); err != nil {
return nil, fmt.Errorf("error reading total coverage response: %v", err)
}
var coverage coveragecalculator.CoverageValues
if err = json.Unmarshal(body, &coverage); err != nil {
return nil, fmt.Errorf("error unmarshalling response to CoverageValues instance: %v", err)
}
return &coverage, nil
}
// GetAndWriteTotalCoverage uses the GetTotalCoverage method to get total coverage and write it to a output file.
func GetAndWriteTotalCoverage(webhookIP string, outputFile string) error {
var (
totalCoverage *coveragecalculator.CoverageValues
err error
)
if totalCoverage, err = GetTotalCoverage(webhookIP); err != nil {
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)
}
return nil
}

View File

@ -0,0 +1,39 @@
# View
This package contains types and helper methods that repos can use to display API
Coverage results.
[DisplayRules](rule.go) provides a mechanism for repos to define their own
display rules. DisplayHelper methods can use these rules to define how to
display results.
`GetHTMLDisplay()` is a utility method that can be used by repos to get a
HTML(JSON) like textual display of API Coverage. This method takes an array of
[TypeCoverage](../coveragecalculator/coveragedata.go) and
[DisplayRules](rule.go) object and returns a string representing its coverage in
the color coded format inside a HTML page:
```
Package: <PackageName>
Type: <TypeName>
{
<FieldName> <Ignored>/<Coverage:TrueorFalse> [Values]
....
....
....
}
```
`GetHTMLCoverageValuesDisplay()` is a utility method that can be used by repos
to produce coverage values display. The method takes as input
[CoverageValue](../coveragecalculator/calculator.go) and produces a display in
the format inside a HTML page:
```
CoverageValues:
Total Fields: <Number of total fields>
Covered Fields: <Number of fields covered>
Ignored Fields: <Number of fields ignored>
Coverage Percentage: <Percentage value of coverage>
```

View File

@ -0,0 +1,131 @@
/*
Copyright 2019 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.
*/
package view
import (
"fmt"
"strings"
"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>
`)
// 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>`))
}
buffer.WriteString(HTMLFooter)
return buffer.String()
}
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))
percentCoverage := 0.0
if coverageValues.TotalFields > 0 {
percentCoverage = (float64(coverageValues.CoveredFields) / float64(coverageValues.TotalFields-coverageValues.IgnoredFields)) * 100
}
buffer.WriteString(fmt.Sprintf(`<tr class="styleheader"><td>Coverage Percentage</td><td>%f</td></tr>`, percentCoverage))
buffer.WriteString(fmt.Sprint(`</table>`))
return buffer.String()
}

View File

@ -0,0 +1,27 @@
/*
Copyright 2019 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.
*/
package view
import "knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
// DisplayRules provides a mechanism for repos to define their own display rules.
// DisplayHelper methods can use these rules to define how to display results.
type DisplayRules struct {
PackageNameRule func(packageName string) string
TypeNameRule func(typeName string) string
FieldRule func(coverage *coveragecalculator.FieldCoverage) string
}

View File

@ -0,0 +1,45 @@
# Webhook
Webhook based API-Coverage tool uses
[ValidatingAdmissionWebhook](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#validatingadmissionwebhook)
which is a web-server that the K8 API-Server calls into for every API-Object
update to verify if the object is valid before storing it into its datastore.
Each validation request has the json representation of the object being
created/modified, that the tool uses to capture coverage data. `webhook` package
inside this folder provides a mechanism for individual repos to setup
ValidatingAdmissionWebhook.
[APICoverageWebhook](webhook.go) type inside the package encapsulates necessary
configuration details and helper methods required to setup the webhook. Each
repo is expected to call into `SetupWebhook()` providing following three
parameters:
1. `http.Handler`: This is the http handler (that implements
`ServeHTTP( w http.ResponseWriter, r *http.Request)`) that the web server
created by APICoverageWebhook uses.
1. `rules`: This is an array of `RuleWithOperations` objects from the
`k8s.io/api/admissionregistration/v1beta1` package that the webhook uses for
validation on each API Object update. e.g: knative-serving while calling this
method would provide rules that will handle API Objects like `Service`,
`Configuration`, `Route` and `Revision`.
1. `namespace`: Namespace name where the webhook would be installed.
1. `stop` channel: Channel to terminate webhook's web server.
`SetupWebhook()` method in its implementation creates a TLS based web server and
registers the webhook by creating a ValidatingWebhookConfiguration object inside
the K8 cluster.
[APICoverageRecorder](apicoverage_recorder.go) type inside the package
encapsulates the apicoverage recording capabilities. Repo using this type is
expected to set:
1. `ResourceForest`: Specifying the version and initializing the
[ResourceTrees](../resourcetree/resourcetree.go)
1. `ResourceMap`: Identifying the resources whose APICoverage needs to be
calculated.
1. `NodeRules`: [NodeRules](../resourcetree/rule.go) that are applicable for the
repo.
1. `FieldRules`: [FieldRules](../resourcetree/rule.go) that are applicable for
the repo.
1. `DisplayRules`: [DisplayRules](../view/rule.go) to be used by
`GetResourceCoverage` method.

View File

@ -0,0 +1,208 @@
/*
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.
*/
package webhook
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"reflect"
"strings"
"go.uber.org/zap"
"k8s.io/api/admission/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"knative.dev/pkg/webhook"
"knative.dev/pkg/test/webhook-apicoverage/coveragecalculator"
"knative.dev/pkg/test/webhook-apicoverage/resourcetree"
"knative.dev/pkg/test/webhook-apicoverage/view"
)
var (
decoder = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
)
const (
// ResourceQueryParam query param name to provide the resource.
ResourceQueryParam = "resource"
// ResourceCoverageEndPoint is the endpoint for Resource Coverage API
ResourceCoverageEndPoint = "/resourcecoverage"
// TotalCoverageEndPoint is the endpoint for Total Coverage API
TotalCoverageEndPoint = "/totalcoverage"
// resourceChannelQueueSize size of the queue maintained for resource channel.
resourceChannelQueueSize = 10
)
type resourceChannelMsg struct {
resourceType schema.GroupVersionKind
rawResourceValue []byte
}
// APICoverageRecorder type contains resource tree to record API coverage for resources.
type APICoverageRecorder struct {
Logger *zap.SugaredLogger
ResourceForest resourcetree.ResourceForest
ResourceMap map[schema.GroupVersionKind]webhook.GenericCRD
NodeRules resourcetree.NodeRules
FieldRules resourcetree.FieldRules
DisplayRules view.DisplayRules
resourceChannel chan resourceChannelMsg
}
// Init initializes the resources trees for set resources.
func (a *APICoverageRecorder) Init() {
for resourceKind, resourceObj := range a.ResourceMap {
a.ResourceForest.AddResourceTree(resourceKind.Kind, reflect.ValueOf(resourceObj).Elem().Type())
}
a.resourceChannel = make(chan resourceChannelMsg, resourceChannelQueueSize)
go a.updateResourceCoverageTree()
}
// updateResourceCoverageTree updates the resource coverage tree.
func (a *APICoverageRecorder) updateResourceCoverageTree() {
for {
channelMsg := <-a.resourceChannel
if err := json.Unmarshal(channelMsg.rawResourceValue, a.ResourceMap[channelMsg.resourceType]); err != nil {
a.Logger.Errorf("Failed unmarshalling review.Request.Object.Raw for type: %s Error: %v", channelMsg.resourceType.Kind, err)
continue
}
resourceTree := a.ResourceForest.TopLevelTrees[channelMsg.resourceType.Kind]
resourceTree.UpdateCoverage(reflect.ValueOf(a.ResourceMap[channelMsg.resourceType]).Elem())
a.Logger.Info("Successfully recorded coverage for resource ", channelMsg.resourceType.Kind)
}
}
// RecordResourceCoverage updates the resource tree with the request.
func (a *APICoverageRecorder) RecordResourceCoverage(w http.ResponseWriter, r *http.Request) {
var (
body []byte
err error
)
review := &v1beta1.AdmissionReview{}
if body, err = ioutil.ReadAll(r.Body); err != nil {
a.Logger.Errorf("Failed reading request body: %v", err)
a.appendAndWriteAdmissionResponse(review, false, "Admission Denied", w)
return
}
if _, _, err := decoder.Decode(body, nil, review); err != nil {
a.Logger.Errorf("Unable to decode request: %v", err)
a.appendAndWriteAdmissionResponse(review, false, "Admission Denied", w)
return
}
gvk := schema.GroupVersionKind{
Group: review.Request.Kind.Group,
Version: review.Request.Kind.Version,
Kind: review.Request.Kind.Kind,
}
// We only care about resources the repo has setup.
if _, ok := a.ResourceMap[gvk]; !ok {
a.Logger.Info("By-passing resource coverage update for resource : %s", gvk.Kind)
a.appendAndWriteAdmissionResponse(review, true, "Welcome Aboard", w)
return
}
a.resourceChannel <- resourceChannelMsg{
resourceType: gvk,
rawResourceValue: review.Request.Object.Raw,
}
a.appendAndWriteAdmissionResponse(review, true, "Welcome Aboard", w)
}
func (a *APICoverageRecorder) appendAndWriteAdmissionResponse(review *v1beta1.AdmissionReview, allowed bool, message string, w http.ResponseWriter) {
review.Response = &v1beta1.AdmissionResponse{
Allowed: allowed,
Result: &v1.Status{
Message: message,
},
}
responseInBytes, err := json.Marshal(review)
if err != nil {
a.Logger.Errorf("Failing mashalling review response: %v", err)
}
if _, err := w.Write(responseInBytes); err != nil {
a.Logger.Errorf("%v", err)
}
}
// GetResourceCoverage retrieves resource coverage data for the passed in resource via query param.
func (a *APICoverageRecorder) GetResourceCoverage(w http.ResponseWriter, r *http.Request) {
resource := r.URL.Query().Get(ResourceQueryParam)
if _, ok := a.ResourceForest.TopLevelTrees[resource]; !ok {
fmt.Fprintf(w, "Resource information not found for resource: %s", resource)
return
}
var ignoredFields coveragecalculator.IgnoredFields
ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
if err := ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
fmt.Fprintf(w, "Error reading file: %s", ignoredFieldsFilePath)
}
tree := a.ResourceForest.TopLevelTrees[resource]
typeCoverage := tree.BuildCoverageData(a.NodeRules, a.FieldRules, ignoredFields)
coverageValues := coveragecalculator.CalculateTypeCoverage(typeCoverage)
var buffer strings.Builder
buffer.WriteString(view.GetHTMLDisplay(typeCoverage, a.DisplayRules))
buffer.WriteString(view.GetHTMLCoverageValuesDisplay(coverageValues))
fmt.Fprint(w, buffer.String())
}
// GetTotalCoverage goes over all the resources setup for the apicoverage tool and returns total coverage values.
func (a *APICoverageRecorder) GetTotalCoverage(w http.ResponseWriter, r *http.Request) {
var (
ignoredFields coveragecalculator.IgnoredFields
err error
)
ignoredFieldsFilePath := os.Getenv("KO_DATA_PATH") + "/ignoredfields.yaml"
if err = ignoredFields.ReadFromFile(ignoredFieldsFilePath); err != nil {
fmt.Fprintf(w, "error reading file: %s error: %v", ignoredFieldsFilePath, err)
}
totalCoverage := coveragecalculator.CoverageValues{}
for resource := range a.ResourceMap {
tree := a.ResourceForest.TopLevelTrees[resource.Kind]
typeCoverage := tree.BuildCoverageData(a.NodeRules, a.FieldRules, ignoredFields)
coverageValues := coveragecalculator.CalculateTypeCoverage(typeCoverage)
totalCoverage.TotalFields += coverageValues.TotalFields
totalCoverage.CoveredFields += coverageValues.CoveredFields
totalCoverage.IgnoredFields += coverageValues.IgnoredFields
}
var body []byte
if body, err = json.Marshal(totalCoverage); err != nil {
fmt.Fprintf(w, "error marshalling total coverage response: %v", err)
}
if _, err = w.Write(body); err != nil {
fmt.Fprintf(w, "error writing total coverage response: %v", err)
}
}

View File

@ -0,0 +1,246 @@
/*
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.
*/
package webhook
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/markbates/inflect"
"go.uber.org/zap"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"knative.dev/pkg/configmap"
"knative.dev/pkg/logging"
"knative.dev/pkg/webhook"
)
var (
// GroupVersionKind for deployment to be used to set the webhook's owner reference.
deploymentKind = extensionsv1beta1.SchemeGroupVersion.WithKind("Deployment")
)
// APICoverageWebhook encapsulates necessary configuration details for the api-coverage webhook.
type APICoverageWebhook struct {
// WebhookName is the name of the validation webhook we create to intercept API calls.
WebhookName string
// ServiceName is the name of K8 service under which the webhook runs.
ServiceName string
// DeploymentName is the deployment name for the webhook.
DeploymentName string
// Namespace is the namespace in which everything above lives.
Namespace string
// Port where the webhook is served.
Port int
// RegistrationDelay controls how long validation requests
// occurs after the webhook is started. This is used to avoid
// potential races where registration completes and k8s apiserver
// invokes the webhook before the HTTP server is started.
RegistrationDelay time.Duration
// ClientAuthType declares the policy the webhook server will follow for TLS Client Authentication.
ClientAuth tls.ClientAuthType
// CaCert is the CA Cert for the webhook server.
CaCert []byte
// FailurePolicy policy governs the webhook validation decisions.
FailurePolicy admissionregistrationv1beta1.FailurePolicyType
// Logger is the configured logger for the webhook.
Logger *zap.SugaredLogger
// KubeClient is the K8 client to the target cluster.
KubeClient kubernetes.Interface
}
func (acw *APICoverageWebhook) generateServerConfig() (*tls.Config, error) {
serverKey, serverCert, caCert, err := webhook.CreateCerts(context.Background(), acw.ServiceName, acw.Namespace)
if err != nil {
return nil, fmt.Errorf("Error creating webhook certificates: %v", err)
}
cert, err := tls.X509KeyPair(serverCert, serverKey)
if err != nil {
return nil, fmt.Errorf("Error creating X509 Key pair for webhook server: %v", err)
}
acw.CaCert = caCert
return &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: acw.ClientAuth,
}, nil
}
func (acw *APICoverageWebhook) getWebhookServer(handler http.Handler) (*http.Server, error) {
tlsConfig, err := acw.generateServerConfig()
if err != nil {
// generateServerConfig() is expected to provided explanatory error message.
return nil, err
}
return &http.Server{
Handler: handler,
Addr: fmt.Sprintf(":%d", acw.Port),
TLSConfig: tlsConfig,
}, nil
}
func (acw *APICoverageWebhook) registerWebhook(rules []admissionregistrationv1beta1.RuleWithOperations, namespace string) error {
webhook := &admissionregistrationv1beta1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: acw.WebhookName,
Namespace: namespace,
},
Webhooks: []admissionregistrationv1beta1.Webhook{{
Name: acw.WebhookName,
Rules: rules,
ClientConfig: admissionregistrationv1beta1.WebhookClientConfig{
Service: &admissionregistrationv1beta1.ServiceReference{
Namespace: namespace,
Name: acw.ServiceName,
},
CABundle: acw.CaCert,
},
FailurePolicy: &acw.FailurePolicy,
},
},
}
deployment, err := acw.KubeClient.ExtensionsV1beta1().Deployments(namespace).Get(acw.DeploymentName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("Error retrieving Deployment Extension object: %v", err)
}
deploymentRef := metav1.NewControllerRef(deployment, deploymentKind)
webhook.OwnerReferences = append(webhook.OwnerReferences, *deploymentRef)
_, err = acw.KubeClient.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(webhook)
if err != nil {
return fmt.Errorf("Error creating ValidatingWebhookConfigurations object: %v", err)
}
return nil
}
func (acw *APICoverageWebhook) getValidationRules(resources map[schema.GroupVersionKind]webhook.GenericCRD) []admissionregistrationv1beta1.RuleWithOperations {
var rules []admissionregistrationv1beta1.RuleWithOperations
for gvk := range resources {
plural := strings.ToLower(inflect.Pluralize(gvk.Kind))
rules = append(rules, admissionregistrationv1beta1.RuleWithOperations{
Operations: []admissionregistrationv1beta1.OperationType{
admissionregistrationv1beta1.Create,
admissionregistrationv1beta1.Update,
},
Rule: admissionregistrationv1beta1.Rule{
APIGroups: []string{gvk.Group},
APIVersions: []string{gvk.Version},
Resources: []string{plural},
},
})
}
return rules
}
// SetupWebhook sets up the webhook with the provided http.handler, resourcegroup Map, namespace and stop channel.
func (acw *APICoverageWebhook) SetupWebhook(handler http.Handler, resources map[schema.GroupVersionKind]webhook.GenericCRD, namespace string, stop <-chan struct{}) error {
server, err := acw.getWebhookServer(handler)
rules := acw.getValidationRules(resources)
if err != nil {
return fmt.Errorf("Webhook server object creation failed: %v", err)
}
select {
case <-time.After(acw.RegistrationDelay):
err = acw.registerWebhook(rules, namespace)
if err != nil {
return fmt.Errorf("Webhook registration failed: %v", err)
}
acw.Logger.Info("Successfully registered webhook")
case <-stop:
return nil
}
serverBootstrapErrCh := make(chan struct{})
go func() {
if err := server.ListenAndServeTLS("", ""); err != nil {
acw.Logger.Error("ListenAndServeTLS for admission webhook returned error", zap.Error(err))
close(serverBootstrapErrCh)
return
}
acw.Logger.Info("Successfully started webhook server")
}()
select {
case <-stop:
return server.Close()
case <-serverBootstrapErrCh:
return errors.New("webhook server bootstrap failed")
}
}
// BuildWebhookConfiguration builds the APICoverageWebhook object using the provided names.
func BuildWebhookConfiguration(componentCommonName string, webhookName string, namespace string) *APICoverageWebhook {
cm, err := configmap.Load("/etc/config-logging")
if err != nil {
log.Fatalf("Error loading logging configuration: %v", err)
}
config, err := logging.NewConfigFromMap(cm)
if err != nil {
log.Fatalf("Error parsing logging configuration: %v", err)
}
logger, _ := logging.NewLoggerFromConfig(config, "webhook")
clusterConfig, err := rest.InClusterConfig()
if err != nil {
log.Fatalf("Failed to get in cluster config: %v", err)
}
kubeClient, err := kubernetes.NewForConfig(clusterConfig)
if err != nil {
log.Fatalf("Failed to get client set: %v", err)
}
return &APICoverageWebhook{
Logger: logger,
KubeClient: kubeClient,
FailurePolicy: admissionregistrationv1beta1.Fail,
ClientAuth: tls.NoClientCert,
RegistrationDelay: time.Second * 2,
Port: 443,
Namespace: namespace,
DeploymentName: componentCommonName,
ServiceName: componentCommonName,
WebhookName: webhookName,
}
}