Skip to content
This repository has been archived by the owner on Sep 5, 2019. It is now read-only.

Use pkg/webhook along with build webhook implementation #379

Merged
merged 18 commits into from
Oct 1, 2018
1 change: 1 addition & 0 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 27 additions & 7 deletions cmd/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ import (
"github.com/knative/build/pkg"
onclusterbuilder "github.com/knative/build/pkg/builder/cluster"
buildclientset "github.com/knative/build/pkg/client/clientset/versioned"

"github.com/knative/build/pkg/apis/build/v1alpha1"
"github.com/knative/build/pkg/webhook"
pkgwebhook "github.com/knative/pkg/webhook"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/knative/pkg/configmap"
"github.com/knative/pkg/logging"
"github.com/knative/pkg/logging/logkey"
Expand Down Expand Up @@ -78,12 +83,27 @@ func main() {
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
bldr := onclusterbuilder.NewBuilder(kubeClient, kubeInformerFactory, logger)

options := webhook.ControllerOptions{
ServiceName: "build-webhook",
ServiceNamespace: pkg.GetBuildSystemNamespace(),
Port: 443,
SecretName: "build-webhook-certs",
WebhookName: "webhook.build.knative.dev",
pkgoptions := pkgwebhook.ControllerOptions{
ServiceName: "build-webhook",
DeploymentName: "build-webhook",
Namespace: pkg.GetBuildSystemNamespace(),
Port: 443,
SecretName: "build-webhook-certs",
WebhookName: "webhook.build.knative.dev",
}
webhook.NewAdmissionController(kubeClient, buildClient, bldr, options, logger).Run(stopCh)

pkgcontroller := pkgwebhook.AdmissionController{
Client: kubeClient,
Options: pkgoptions,
Handlers: map[schema.GroupVersionKind]pkgwebhook.GenericCRD{
v1alpha1.SchemeGroupVersion.WithKind("Build"): &v1alpha1.Build{},
v1alpha1.SchemeGroupVersion.WithKind("ClusterBuildTemplate"): &v1alpha1.ClusterBuildTemplate{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm that the common infrastructure works properly with cluster-scoped resources, since AFAIK this is the first one we're trying :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool 👍
I have added e2e test for build with template and build with cluster template as well. Going forward we will have better coverage for these features

v1alpha1.SchemeGroupVersion.WithKind("BuildTemplate"): &v1alpha1.BuildTemplate{},
},
Logger: logger,
}
go pkgcontroller.Run(stopCh)

buildWebhookController := webhook.NewAdmissionController(kubeClient, buildClient, bldr, pkgoptions, logger)
go buildWebhookController.Run(stopCh)
}
20 changes: 20 additions & 0 deletions pkg/apis/build/v1alpha1/build_defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package v1alpha1

import (
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// DefaultTime is 10min
const DefaultTime = 10 * time.Minute
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: DefaultTimeout


// SetDefaults for build
func (b *Build) SetDefaults() {
shashwathi marked this conversation as resolved.
Show resolved Hide resolved
if b.Spec.ServiceAccountName == "" {
b.Spec.ServiceAccountName = "default"
}
if b.Spec.Timeout.Duration == 0 {
b.Spec.Timeout = metav1.Duration{Duration: DefaultTime}
}
}
16 changes: 16 additions & 0 deletions pkg/apis/build/v1alpha1/build_defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package v1alpha1

import (
"testing"
)

func TestSetDefault(t *testing.T) {
shashwathi marked this conversation as resolved.
Show resolved Hide resolved
emptyBuild := &Build{}
emptyBuild.SetDefaults()
if emptyBuild.Spec.ServiceAccountName != "default" {
t.Errorf("Expect default to be the serviceaccount name but got %s", emptyBuild.Spec.ServiceAccountName)
}
if emptyBuild.Spec.Timeout.Duration != DefaultTime {
t.Errorf("Expect build timeout to be set")
}
}
6 changes: 6 additions & 0 deletions pkg/apis/build/v1alpha1/build_template_defaults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package v1alpha1

// SetDefaults for build template
func (b *BuildTemplate) SetDefaults() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just put this into build_template_types.go ? This separate file doesn't add much.

return
}
5 changes: 5 additions & 0 deletions pkg/apis/build/v1alpha1/build_template_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1alpha1
import (
"encoding/json"

"github.com/knative/pkg/apis"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -48,6 +49,10 @@ type BuildTemplate struct {
var _ kmeta.OwnerRefable = (*BuildTemplate)(nil)
var _ Template = (*BuildTemplate)(nil)

// Check that BuildTemplate may be validated and defaulted.
var _ apis.Validatable = (*BuildTemplate)(nil)
var _ apis.Defaultable = (*BuildTemplate)(nil)

// BuildTemplateSpec is the spec for a BuildTemplate.
type BuildTemplateSpec struct {
// TODO: Generation does not work correctly with CRD. They are scrubbed
Expand Down
12 changes: 12 additions & 0 deletions pkg/apis/build/v1alpha1/build_template_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ func TestBuildTemplateGroupVersionKind(t *testing.T) {
t.Errorf("GetGroupVersionKind mismatch; expected: %v got: %v", expectedKind, c.GetGroupVersionKind().Kind)
}
}

func TestBuildTemplateGeneration(t *testing.T) {
c := BuildTemplate{}
if a := c.GetGeneration(); a != 0 {
t.Errorf("empty build template generation should be 0 but got: %d", a)
}

c.SetGeneration(5)
if e, a := int64(5), c.GetGeneration(); e != a {
t.Errorf("getgeneration mismatch; expected: %d got: %d", e, a)
}
}
91 changes: 91 additions & 0 deletions pkg/apis/build/v1alpha1/build_template_validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package v1alpha1

import (
"strings"

"github.com/knative/pkg/apis"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

corev1 "k8s.io/api/core/v1"
)

// Validate build template
func (b *BuildTemplate) Validate() *apis.FieldError {
return validateObjectMetadata(b.GetObjectMeta()).ViaField("metadata").Also(b.Spec.Validate().ViaField("spec"))
}

// Validate Build Template
func (b *BuildTemplateSpec) Validate() *apis.FieldError {
if err := validateSteps(b.Steps); err != nil {
return err
}
if err := validateVolumes(b.Volumes); err != nil {
return err
}
if err := validateParameters(b.Parameters); err != nil {
return err
}
return nil
}

func validateObjectMetadata(meta metav1.Object) *apis.FieldError {
name := meta.GetName()

if strings.Contains(name, ".") {
return &apis.FieldError{
Message: "Invalid resource name: special character . must not be present",
Paths: []string{"name"},
}
}

if len(name) > 63 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract this to a const?

return &apis.FieldError{
Message: "Invalid resource name: length must be no more than 63 characters",
Paths: []string{"name"},
}
}
return nil
}

func validateParameters(params []ParameterSpec) *apis.FieldError {
// Template must not duplicate parameter names.
seen := map[string]struct{}{}
for _, p := range params {
if _, ok := seen[p.Name]; ok {
return apis.ErrMultipleOneOf("ParamName")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think MultipleOneOf is the correct error, I think that error is intended to communicate "you can specify X or Y, but not both"

https://godoc.org/github.com/knative/pkg/apis#ErrMultipleOneOf

(ErrMissingOneOf is "you must specify X or Y, but not neither")

Maybe ErrInvalidKeyName? https://godoc.org/github.com/knative/pkg/apis#ErrInvalidKeyName

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah pick the error with closest match I guess. I am okay with ErrInvalidKeyName

}
seen[p.Name] = struct{}{}
}
return nil
}

func validateVolumes(volumes []corev1.Volume) *apis.FieldError {
// Build must not duplicate volume names.
vols := map[string]struct{}{}
for _, v := range volumes {
if _, ok := vols[v.Name]; ok {
return apis.ErrMultipleOneOf("volumeName")
}
vols[v.Name] = struct{}{}
}
return nil
}

func validateSteps(steps []corev1.Container) *apis.FieldError {
// Build must not duplicate step names.
names := map[string]struct{}{}
for _, s := range steps {
if s.Image == "" {
return apis.ErrMissingField("Image")
}

if s.Name == "" {
continue
}
if _, ok := names[s.Name]; ok {
return apis.ErrMultipleOneOf("stepName")
}
names[s.Name] = struct{}{}
}
return nil
}
127 changes: 127 additions & 0 deletions pkg/apis/build/v1alpha1/build_template_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package v1alpha1

import (
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestValidateTemplate(t *testing.T) {
hasDefault := "has-default"
for _, c := range []struct {
desc string
tmpl *BuildTemplate
reason string // if "", expect success.
}{{
desc: "Single named step",
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Steps: []corev1.Container{{
Name: "foo",
Image: "gcr.io/foo-bar/baz:latest",
}},
},
},
}, {
desc: "long name",
reason: "long name",
tmpl: &BuildTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "*********************************************************************************************",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.Repeat("x", maxLength+1) is self-documenting and self-updating if we update maxLength.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed my TODO there. Good catch

},
},
}, {
desc: "invalid name",
reason: "invalid name",
tmpl: &BuildTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: "b.c",
},
},
}, {
desc: "Multiple unnamed steps",
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Steps: []corev1.Container{{
Image: "gcr.io/foo-bar/baz:latest",
}, {
Image: "gcr.io/foo-bar/baz:latest",
}, {
Image: "gcr.io/foo-bar/baz:latest",
}},
},
},
}, {
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Steps: []corev1.Container{{
Name: "foo",
Image: "gcr.io/foo-bar/baz:latest",
}, {
Name: "foo",
Image: "gcr.io/foo-bar/baz:oops",
}},
},
},
reason: "DuplicateStepName",
}, {
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Steps: []corev1.Container{{
Name: "foo",
Image: "gcr.io/foo-bar/baz:latest",
}},
Volumes: []corev1.Volume{{
Name: "foo",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
}, {
Name: "foo",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
}},
},
},
reason: "DuplicateVolumeName",
}, {
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Parameters: []ParameterSpec{{
Name: "foo",
}, {
Name: "foo",
}},
},
},
reason: "DuplicateParamName",
}, {
tmpl: &BuildTemplate{
Spec: BuildTemplateSpec{
Steps: []corev1.Container{{
Name: "step-name-${FOO${BAR}}",
}},
Parameters: []ParameterSpec{{
Name: "FOO",
}, {
Name: "BAR",
Default: &hasDefault,
}},
},
},
reason: "NestedPlaceholder",
}} {
name := c.desc
if c.reason != "" {
name = "invalid-" + c.reason
}
t.Run(name, func(t *testing.T) {
verr := c.tmpl.Validate()
if gotErr, wantErr := verr != nil, c.reason != ""; gotErr != wantErr {
t.Errorf("validateBuildTemplate(%s); got %v, want %q", name, verr, c.reason)
}
})
}
}
5 changes: 5 additions & 0 deletions pkg/apis/build/v1alpha1/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"

"github.com/knative/pkg/apis"
duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1"
"github.com/knative/pkg/kmeta"
)
Expand All @@ -45,6 +46,10 @@ type Build struct {
// Check that our resource implements several interfaces.
var _ kmeta.OwnerRefable = (*Build)(nil)

// Check that Build may be validated and defaulted.
var _ apis.Validatable = (*Build)(nil)
var _ apis.Defaultable = (*Build)(nil)

// BuildSpec is the spec for a Build resource.
type BuildSpec struct {
// TODO: Generation does not work correctly with CRD. They are scrubbed
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/build/v1alpha1/build_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ func TestBuildConditions(t *testing.T) {
t.Errorf("Unexpected build condition type; want %v got %v", want, b.Status.GetConditions())
}

fooStatus := b.Status.GetCondition(foo.Type)
if cmp.Diff(fooStatus, foo, ignoreVolatileTime) != "" {
t.Errorf("Unexpected build condition type; want %v got %v", fooStatus, foo)
}

// Add a second condition.
b.Status.SetCondition(bar)

Expand Down
Loading