Skip to content

Commit

Permalink
Setup webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
SkYNewZ committed Sep 15, 2022
1 parent 469fd33 commit c33e71c
Show file tree
Hide file tree
Showing 27 changed files with 764 additions and 200 deletions.
7 changes: 0 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@ jobs:
- name: Run tests
run: make test

integration-tests:
runs-on: ubuntu-latest
name: Kubernetes tests
steps:
- name: Setup kind
run: echo "Running integration tests"

lint:
runs-on: ubuntu-latest
name: Go lint
Expand Down
4 changes: 4 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ resources:
kind: Feed
path: github.com/SkYNewZ/putio-operator/api/v1alpha1
version: v1alpha1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,3 @@ 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.

## TODOs

- [ ] Write integration tests
- [ ] Run integrations tests in CI
38 changes: 19 additions & 19 deletions api/v1alpha1/feed_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,38 @@ type AuthSecretReference struct {
// FeedSpec defines the desired state of Feed.
type FeedSpec struct {
// +kubebuilder:validation:MinLength:=1
// Title of the RSS feed as will appear on the site
// Title of the RSS feed as will appear on the site.
Title string `json:"title"`

// +kubebuilder:validation:MinLength:=1
// The URL of the RSS feed to be watched
// The URL of the RSS feed to be watched.
RssSourceURL string `json:"rss_source_url"`

// +kubebuilder:default:=0
// The file ID of the folder to place the RSS feed files in
ParentDirID *uint32 `json:"parent_dir_id,omitempty"`
// The file ID of the folder to place the RSS feed files in. Default to the root directory (0).
// +optional
ParentDirID *uint `json:"parent_dir_id,omitempty"`

// +kubebuilder:default:=false
// Should old files in the folder be deleted when space is low
DeleteOldFiles bool `json:"delete_old_files,omitempty"`
// Should old files in the folder be deleted when space is low. Default to false.
// +optional
DeleteOldFiles *bool `json:"delete_old_files,omitempty"`

// +kubebuilder:default:=false
// Should the current items in the feed, at creation time, be ignored
DontProcessWholeFeed bool `json:"dont_process_whole_feed,omitempty"`
// Should the current items in the feed, at creation time, be ignored.
// +optional
DontProcessWholeFeed *bool `json:"dont_process_whole_feed,omitempty"`

// +kubebuilder:validation:MinLength:=1
// Only items with titles that contain any of these words will be transferred (comma-separated list of words)
// Only items with titles that contain any of these words will be transferred (comma-separated list of words).
Keyword string `json:"keyword"`

// No items with titles that contain any of these words will be transferred (comma-separated list of words).
// +optional
// No items with titles that contain any of these words will be transferred (comma-separated list of words)
UnwantedKeywords string `json:"unwanted_keywords,omitempty"`

// +kubebuilder:default:=false
// Should the RSS feed be created in the paused state
Paused bool `json:"paused,omitempty"`
// Should the RSS feed be created in the paused state. Default to false.
// +optional
Paused *bool `json:"paused,omitempty"`

// Authentication reference to Put.io token in a secret
// Authentication reference to Put.io token in a secret.
AuthSecretRef AuthSecretReference `json:"authSecretRef"`
}

Expand Down Expand Up @@ -94,8 +94,8 @@ type Feed struct {
Status FeedStatus `json:"status,omitempty"`
}

func (in *Feed) AuthSecretRef() AuthSecretReference {
return in.Spec.AuthSecretRef
func (r *Feed) AuthSecretRef() AuthSecretReference {
return r.Spec.AuthSecretRef
}

//+kubebuilder:object:root=true
Expand Down
6 changes: 3 additions & 3 deletions api/v1alpha1/feed_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ func TestFeed_AuthSecretRef(t *testing.T) {
Title: "",
RssSourceURL: "",
ParentDirID: nil,
DeleteOldFiles: false,
DontProcessWholeFeed: false,
DeleteOldFiles: new(bool),
DontProcessWholeFeed: new(bool),
Keyword: "",
UnwantedKeywords: "",
Paused: false,
Paused: new(bool),
AuthSecretRef: AuthSecretReference{
Name: "foo",
Key: "bar",
Expand Down
123 changes: 123 additions & 0 deletions api/v1alpha1/feed_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Copyright 2022 Quentin Lemaire <[email protected]>.
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 v1alpha1

import (
"context"
"net/url"

"go.opentelemetry.io/otel"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

var (
// log is for logging in this package.
feedlog = logf.Log.WithName("feed-resource")
tracer = otel.GetTracerProvider().Tracer("webhook")
)

const defaultParentDirID uint = 0

func (r *Feed) SetupWebhookWithManager(mgr ctrl.Manager) error {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.SetupWebhookWithManager")
defer span.End()

//nolint:wrapcheck
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

//+kubebuilder:webhook:path=/mutate-putio-skynewz-dev-v1alpha1-feed,mutating=true,failurePolicy=fail,sideEffects=None,groups=putio.skynewz.dev,resources=feeds,verbs=create;update,versions=v1alpha1,name=mfeed.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &Feed{}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
func (r *Feed) Default() {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.Default")
defer span.End()

feedlog.Info("default", "name", r.Name)

if r.Spec.ParentDirID == nil {
r.Spec.ParentDirID = new(uint)
*r.Spec.ParentDirID = defaultParentDirID
}

if r.Spec.DeleteOldFiles == nil {
r.Spec.DeleteOldFiles = new(bool)
}

if r.Spec.DontProcessWholeFeed == nil {
r.Spec.DontProcessWholeFeed = new(bool)
}

if r.Spec.Paused == nil {
r.Spec.Paused = new(bool)
}
}

//+kubebuilder:webhook:path=/validate-putio-skynewz-dev-v1alpha1-feed,mutating=false,failurePolicy=fail,sideEffects=None,groups=putio.skynewz.dev,resources=feeds,verbs=create;update,versions=v1alpha1,name=vfeed.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Feed{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (r *Feed) ValidateCreate() error {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.ValidateCreate")
defer span.End()

feedlog.Info("validate create", "name", r.Name)
return r.validateFeedSpec()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (r *Feed) ValidateUpdate(_ runtime.Object) error {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.ValidateUpdate")
defer span.End()

feedlog.Info("validate update", "name", r.Name)
return r.validateFeedSpec()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (r *Feed) ValidateDelete() error {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.ValidateDelete")
defer span.End()

feedlog.Info("validate delete", "name", r.Name)
return nil // nothing to validate on deletion
}

func (r *Feed) validateFeedSpec() error {
_, span := tracer.Start(context.Background(), "v1alpha1.Feed.validateFeedSpec")
defer span.End()

// validate URL
return r.validateRSSSourceURL(r.Spec.RssSourceURL, field.NewPath("spec").Child("rss_source_url"))
}

func (r *Feed) validateRSSSourceURL(u string, fldPath *field.Path) error {
if _, err := url.ParseRequestURI(u); err != nil {
return field.Invalid(fldPath, u, "invalid URL provided")
}

return nil
}
134 changes: 134 additions & 0 deletions api/v1alpha1/feed_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package v1alpha1

import (
"context"
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)

func TestFeed_validateRSSSourceURL(t *testing.T) {
type fields struct {
TypeMeta v1.TypeMeta
ObjectMeta v1.ObjectMeta
Spec FeedSpec
Status FeedStatus
}
type args struct {
u string
fldPath *field.Path
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "valid URL",
fields: fields{},
args: args{
u: "https://google.fr",
fldPath: field.NewPath("spec").Child("rss_source_url"),
},
wantErr: false,
},
{
name: "invalid url",
fields: fields{},
args: args{
u: "foo bar",
fldPath: field.NewPath("spec").Child("rss_source_url"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &Feed{
TypeMeta: tt.fields.TypeMeta,
ObjectMeta: tt.fields.ObjectMeta,
Spec: tt.fields.Spec,
Status: tt.fields.Status,
}
if err := r.validateRSSSourceURL(tt.args.u, tt.args.fldPath); (err != nil) != tt.wantErr {
t.Errorf("validateRSSSourceURL() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

var _ = Describe("Feed webhook", func() {
// Define utility constants for object names and testing timeouts/durations and intervals.
const (
FeedName = "test-cronjob"
FeedNamespace = "default"
)

Context("When creating Feed", func() {
It("Should validate Feed URL", func() {
By("By giving a wrong URL")
ctx := context.Background()
feed := &Feed{
TypeMeta: v1.TypeMeta{
Kind: "Feed",
APIVersion: "putio.skynewz.dev/v1alpha1",
},
ObjectMeta: v1.ObjectMeta{
Name: FeedName,
Namespace: FeedNamespace,
},
Spec: FeedSpec{
Title: "foo",
RssSourceURL: "foo bar",
ParentDirID: nil,
DeleteOldFiles: new(bool),
DontProcessWholeFeed: new(bool),
Keyword: "foo",
UnwantedKeywords: "",
Paused: new(bool),
AuthSecretRef: AuthSecretReference{
Name: "putio-token",
Key: "token",
},
},
Status: FeedStatus{},
}

expectedError := "admission webhook \"vfeed.kb.io\" denied the request: spec.rss_source_url: Invalid value: \"foo bar\": invalid URL provided"
Expect(k8sClient.Create(ctx, feed)).Should(MatchError(expectedError))

By("By giving a valid URL")
feed = &Feed{
TypeMeta: v1.TypeMeta{
Kind: "Feed",
APIVersion: "putio.skynewz.dev/v1alpha1",
},
ObjectMeta: v1.ObjectMeta{
Name: FeedName,
Namespace: FeedNamespace,
},
Spec: FeedSpec{
Title: "foo",
RssSourceURL: "https://www.google.fr",
ParentDirID: nil,
DeleteOldFiles: new(bool),
DontProcessWholeFeed: new(bool),
Keyword: "foo",
UnwantedKeywords: "",
Paused: new(bool),
AuthSecretRef: AuthSecretReference{
Name: "putio-token",
Key: "token",
},
},
Status: FeedStatus{},
}

Expect(k8sClient.Create(ctx, feed)).Should(Succeed())
})
})
})
Loading

0 comments on commit c33e71c

Please sign in to comment.