Skip to content

Commit

Permalink
Add BMC subscription CRD and reconciler
Browse files Browse the repository at this point in the history
This patch implements the fast eventing API:

metal3-io/metal3-docs#167

We introduce a new CRD, and a new reconciler.  We use the Ironic API to
communicate with the BMC to manage node subscriptions.
  • Loading branch information
honza committed Dec 13, 2021
1 parent 9db43c4 commit 8490f11
Show file tree
Hide file tree
Showing 29 changed files with 1,119 additions and 5 deletions.
9 changes: 9 additions & 0 deletions PROJECT
Expand Up @@ -44,4 +44,13 @@ resources:
kind: PreprovisioningImage
path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: metal3.io
group: metal3.io
kind: BMCEventSubscription
path: github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1
version: v1alpha1
version: "3"
1 change: 1 addition & 0 deletions apis/go.mod
Expand Up @@ -4,6 +4,7 @@ go 1.17

require (
github.com/metal3-io/baremetal-operator/pkg/hardwareutils v0.0.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.7.0
k8s.io/api v0.21.4
k8s.io/apimachinery v0.21.4
Expand Down
86 changes: 86 additions & 0 deletions apis/metal3.io/v1alpha1/bmceventsubscription_types.go
@@ -0,0 +1,86 @@
/*
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 (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (

// BMCEventSubscriptionFinalizer is the name of the finalizer added to
// subscriptions to block delete operations until the subscription is removed
// from the BMC.
BMCEventSubscriptionFinalizer string = "bmceventsubscription.metal3.io"
)

type BMCEventSubscriptionSpec struct {
// A reference to a BareMetalHost
HostName string `json:"hostName,omitempty"`

// A webhook URL to send events to
Destination string `json:"destination,omitempty"`

// Arbitrary user-provided context for the event
Context string `json:"context,omitempty"`

// Messages of which type we should pass along; defaults to ["Alert"]
EventTypes []string `json:"eventTypes,omitempty"`

// The BMC protocol to use; defaults to "Redfish"
Protocol string `json:"protocol,omitempty"`

// A secret containing HTTP headers which should be passed along to the Destination
// when making a request
HTTPHeadersRef *corev1.SecretReference `json:"httpHeadersRef,omitempty"`
}

type BMCEventSubscriptionStatus struct {
SubscriptionID string `json:"subscriptionID,omitempty"`
Error string `json:"error,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
//
// BMCEventSubscription is the Schema for the fast eventing API
// +k8s:openapi-gen=true
// +kubebuilder:resource:shortName=bes;bmcevent
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Error",type="string",JSONPath=".status.error",description="The most recent error message"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Time duration since creation of BMCEventSubscription"
// +kubebuilder:object:root=true
type BMCEventSubscription struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec BMCEventSubscriptionSpec `json:"spec,omitempty"`
Status BMCEventSubscriptionStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// BMCEventSubscriptionList contains a list of BMCEventSubscriptions
type BMCEventSubscriptionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BMCEventSubscription `json:"items"`
}

func init() {
SchemeBuilder.Register(&BMCEventSubscription{}, &BMCEventSubscriptionList{})
}
59 changes: 59 additions & 0 deletions apis/metal3.io/v1alpha1/bmceventsubscription_validation.go
@@ -0,0 +1,59 @@
/*
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 (
"fmt"
"net/url"

"github.com/pkg/errors"

logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// bmclog is for logging in this package.
var bmclog = logf.Log.WithName("bmceventsubscription-validation")

// validateSubscription validates BMCEventSubscription resource for creation
func (s *BMCEventSubscription) validateSubscription() []error {
bmclog.Info("validate create", "name", s.Name)
var errs []error

if s.Spec.HostName == "" {
errs = append(errs, fmt.Errorf("HostName cannot be empty"))
}

if s.Spec.Destination == "" {
errs = append(errs, fmt.Errorf("Destination cannot be empty"))
} else {
destinationUrl, err := url.ParseRequestURI(s.Spec.Destination)

if err != nil {
errs = append(errs, errors.Wrap(err, "Destination is an invalid URL"))
} else {
// Require a trailing slash on a domain without a path:
//
// Good: https://localhost:1234/index.php
// Good: https://localhost:1234/
// Bad: https://localhost:1234
if destinationUrl.Path == "" {
errs = append(errs, fmt.Errorf("Hostname-only destination must have a trailing slash"))
}
}
}

return errs
}
81 changes: 81 additions & 0 deletions apis/metal3.io/v1alpha1/bmceventsubscription_validation_test.go
@@ -0,0 +1,81 @@
package v1alpha1

import (
"testing"

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

func TestBMCEventSubscriptionValidateCreate(t *testing.T) {
tm := metav1.TypeMeta{
Kind: "BMCEventSubscription",
APIVersion: "metal3.io/v1alpha1",
}

om := metav1.ObjectMeta{
Name: "test",
Namespace: "test-namespace",
}

tests := []struct {
name string
newS *BMCEventSubscription
oldS *BMCEventSubscription
wantedErr string
}{
{
name: "valid",
newS: &BMCEventSubscription{TypeMeta: tm, ObjectMeta: om, Spec: BMCEventSubscriptionSpec{HostName: "worker-01", Destination: "http://localhost/abc/abc.php"}},
oldS: nil,
wantedErr: "",
},
{
name: "missingHostName",
newS: &BMCEventSubscription{
TypeMeta: tm,
ObjectMeta: om,
Spec: BMCEventSubscriptionSpec{Destination: "http://localhost/abc/abc"},
},
oldS: nil,
wantedErr: "HostName cannot be empty",
},
{
name: "missingDestination",
newS: &BMCEventSubscription{
TypeMeta: tm,
ObjectMeta: om,
Spec: BMCEventSubscriptionSpec{HostName: "worker-01"},
},
oldS: nil,
wantedErr: "Destination cannot be empty",
},
{
name: "destinationNotUrl",
newS: &BMCEventSubscription{
TypeMeta: tm,
ObjectMeta: om,
Spec: BMCEventSubscriptionSpec{HostName: "worker-01", Destination: "abc"},
},
oldS: nil,
wantedErr: "Destination is an invalid URL: parse \"abc\": invalid URI for request",
},
{
name: "destinationMissingTrailingSlash",
newS: &BMCEventSubscription{
TypeMeta: tm,
ObjectMeta: om,
Spec: BMCEventSubscriptionSpec{HostName: "worker-01", Destination: "http://localhost"},
},
oldS: nil,
wantedErr: "Hostname-only destination must have a trailing slash",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.newS.validateSubscription(); !errorArrContains(err, tt.wantedErr) {
t.Errorf("BareMetalHost.validateSubscription() error = %v, wantErr %v", err, tt.wantedErr)
}
})
}
}
54 changes: 54 additions & 0 deletions apis/metal3.io/v1alpha1/bmceventsubscription_webhook.go
@@ -0,0 +1,54 @@
/*
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 (
"fmt"

"k8s.io/apimachinery/pkg/util/errors"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// bmcsubscriptionlog is for logging in this package.
var bmcsubscriptionlog = logf.Log.WithName("bmceventsubscription-resource")

func (s *BMCEventSubscription) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(s).
Complete()
}

//+kubebuilder:webhook:verbs=create;update,path=/validate-metal3-io-v1alpha1-bmceventsubscription,mutating=false,failurePolicy=fail,sideEffects=none,admissionReviewVersions=v1;v1beta,groups=metal3.io,resources=bmceventsubscriptions,versions=v1alpha1,name=bmceventsubscription.metal3.io

var _ webhook.Validator = &BMCEventSubscription{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (s *BMCEventSubscription) ValidateCreate() error {
bmcsubscriptionlog.Info("validate create", "name", s.Name)
return errors.NewAggregate(s.validateSubscription())
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (s *BMCEventSubscription) ValidateUpdate(old runtime.Object) error {
bmcsubscriptionlog.Info("validate update", "name", s.Name)
return fmt.Errorf("subscriptions cannot be updated, please recreate it")
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (s *BMCEventSubscription) ValidateDelete() error {
return nil
}

0 comments on commit 8490f11

Please sign in to comment.