Skip to content

Commit

Permalink
Merge pull request #88 from bjwswang/74-add-webhook-to-channel
Browse files Browse the repository at this point in the history
feat: new/add webhooks to channel\organization\federation;fix dir per…
  • Loading branch information
bjwswang committed Feb 8, 2023
2 parents 610fb0c + da2f081 commit 052eefe
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 6 deletions.
4 changes: 4 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,8 @@ resources:
kind: Channel
path: github.com/IBM-Blockchain/fabric-operator/api/v1beta1
version: v1beta1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
196 changes: 196 additions & 0 deletions api/v1beta1/channel_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright contributors to the Hyperledger Fabric Operator project
*
* SPDX-License-Identifier: Apache-2.0
*
* 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 v1beta1

import (
"context"

"github.com/pkg/errors"
authenticationv1 "k8s.io/api/authentication/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
)

var (
errNoNetwork = errors.New("cant find network")
errMemberNotInNetwork = errors.New("some channel member not in network")
errNoPermOperatePeer = errors.New("no permission to operate peer")
errUpdateChannelNetwork = errors.New("cant update channel's network")
errUpdateChannelMember = errors.New("cant update channel's members directly(must use proposal-vote)")
errChannelHasPeers = errors.New("channel still have peers joined")
)

// log is for logging in this package.
var channellog = logf.Log.WithName("channel-resource")

//+kubebuilder:webhook:path=/mutate-ibp-com-v1beta1-channel,mutating=true,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=channels,verbs=create;update,versions=v1beta1,name=channel.mutate.webhook,admissionReviewVersions=v1

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Channel) Default(ctx context.Context, client client.Client, user authenticationv1.UserInfo) {
channellog.Info("default", "name", r.Name, "user", user.String())
}

//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-channel,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=channels,verbs=create;update;delete,versions=v1beta1,name=channel.validate.webhook,admissionReviewVersions=v1

var _ validator = &Channel{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Channel) ValidateCreate(ctx context.Context, c client.Client, user authenticationv1.UserInfo) error {
channellog.Info("validate create", "name", r.Name, "user", user.String())

err := validateMemberInNetwork(ctx, c, r.Spec.Network, r.Spec.Members)
if err != nil {
return err
}

// managedOrgs which this user can manage
managedOrgs, err := filterManagedOrgs(ctx, c, user, r.Spec.Members)
if err != nil {
return err
}
// initialized peers should under user's management
err = validatePeersOwnership(ctx, c, managedOrgs, r.Spec.Peers)
if err != nil {
return err
}

return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Channel) ValidateUpdate(ctx context.Context, c client.Client, old runtime.Object, user authenticationv1.UserInfo) error {
channellog.Info("validate update", "name", r.Name, "user", user.String())

oldChannel := old.(*Channel)

// forbid to udpate channel network
if oldChannel.Spec.Network != r.Spec.Network {
return errUpdateChannelNetwork
}

// forbid to update channel members directly
added, removed := DifferMembers(oldChannel.Spec.Members, r.Spec.Members)
if len(added) != 0 || len(removed) != 0 {
return errUpdateChannelMember
}

// forbid to update peers which not belongs to user's organizations
addedPeers, removedPeers := DifferChannelPeers(oldChannel.Spec.Peers, r.Spec.Peers)
if len(addedPeers) != 0 || len(removedPeers) != 0 {
managedOrgs, err := filterManagedOrgs(ctx, c, user, r.Spec.Members)
if err != nil {
return err
}
// updated peers should under user's management
err = validatePeersOwnership(ctx, c, managedOrgs, append(addedPeers, removedPeers...))
if err != nil {
return err
}
}

return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Channel) ValidateDelete(ctx context.Context, client client.Client, user authenticationv1.UserInfo) error {
channellog.Info("validate delete", "name", r.Name, "user", user.String())

// forbid to delete channel if still have peers joined
if len(r.Spec.Peers) != 0 {
return errors.Wrapf(errChannelHasPeers, "count %d", len(r.Spec.Peers))
}

return nil
}

func validateMemberInNetwork(ctx context.Context, c client.Client, netName string, members []Member) error {
net := &Network{}
net.Name = netName
if err := c.Get(ctx, client.ObjectKeyFromObject(net), net); err != nil {
if apierrors.IsNotFound(err) {
return errNoNetwork
}
return errors.Wrap(err, "failed to get network")
}
if net.Status.Type == Error {
return errors.Errorf("network %s has error %s:%s", netName, net.Status.Reason, net.Status.Message)
}

allMembers := make(map[string]bool, len(net.Spec.Members))
for _, m := range net.Spec.Members {
allMembers[m.Name] = true
}
for _, m := range members {
if ok := allMembers[m.Name]; !ok {
return errors.Wrapf(errMemberNotInNetwork, "allMembers:%#v, members:%#v", allMembers, members)
}
}
return nil
}

// filterManagedOrgs will get the organizations which under user's management
func filterManagedOrgs(ctx context.Context, c client.Client, user authenticationv1.UserInfo, members []Member) ([]string, error) {
var err error

// managedOrgs which this user can manage
managedOrgs := make([]string, 0)
// validate ownership
for _, member := range members {
org := &Organization{}
org.Name = member.Name
err = c.Get(ctx, client.ObjectKeyFromObject(org), org)
if err != nil {
return nil, errors.Wrap(err, "failed to get organization")
}
if isSuperUser(ctx, user) || org.Spec.Admin == user.Username {
managedOrgs = append(managedOrgs, member.Name)
}
}

return managedOrgs, nil
}

// validatePeersOwnership validate whether peers belongs to ownerOrgs
func validatePeersOwnership(ctx context.Context, c client.Client, ownerOrgs []string, peers []NamespacedName) error {
// cache owners
owners := make(map[string]bool, len(ownerOrgs))
for _, ownerOrg := range ownerOrgs {
owners[ownerOrg] = true
}
// make sure peers run in ownerOrgs
for _, peer := range peers {
// peer must belongs to owners
if !owners[peer.Namespace] {
return errors.Wrapf(errNoPermOperatePeer, "peer belongs to %s not in %v", peer.Namespace, ownerOrgs)
}
p := &IBPPeer{}
err := c.Get(ctx, types.NamespacedName{Namespace: peer.Namespace, Name: peer.Name}, p)
if err != nil {
return errors.Wrapf(err, "failed to get peer %s", peer.String())
}
if p.Status.Type == Error {
return errors.Errorf("peer %s has error %s:%s", peer.String(), p.Status.Reason, p.Status.Message)
}
}

return nil
}
33 changes: 32 additions & 1 deletion api/v1beta1/federation_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (r *Federation) Default(ctx context.Context, client client.Client, user aut
federationlog.Info("default", "name", r.Name, "user", user.String())
}

//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-federation,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.ibp.com,resources=federations,verbs=create;update;delete,versions=v1beta1,name=federation.validate.webhook,admissionReviewVersions=v1
//+kubebuilder:webhook:path=/validate-ibp-com-v1beta1-federation,mutating=false,failurePolicy=fail,sideEffects=None,groups=ibp.com,resources=federations,verbs=create;update;delete,versions=v1beta1,name=federation.validate.webhook,admissionReviewVersions=v1

var _ validator = &Federation{}

Expand All @@ -64,6 +64,13 @@ func (r *Federation) ValidateCreate(ctx context.Context, client client.Client, u
return err
}

for _, member := range r.Spec.Members {
err := validateOrganization(ctx, client, member.Name)
if err != nil {
return err
}
}

return nil
}

Expand All @@ -90,6 +97,14 @@ func (r *Federation) ValidateDelete(ctx context.Context, client client.Client, u
return err
}

if r.Status.Type != FederationFailed && r.Status.Type != FederationDissolved {
return errors.Errorf("forbid to delete federation when it is at status %s", r.Status.Type)
}

if len(r.Status.Networks) != 0 {
return errors.Errorf("forbid to delete federation when it still has %d networks", len(r.Status.Networks))
}

return nil
}

Expand Down Expand Up @@ -118,3 +133,19 @@ func validateInitiator(ctx context.Context, c client.Client, user authentication
}
return nil
}

func validateOrganization(ctx context.Context, c client.Client, organization string) error {
federationlog.Info("validate organization: %s", organization)
org := &Organization{}
org.Name = organization
err := c.Get(ctx, client.ObjectKeyFromObject(org), org)
if err != nil {
return errors.Wrapf(err, "failed to get organization %s", organization)
}

if org.Status.Type == Error {
return errors.Errorf("organization %s has error %s:%s", org.Name, org.Status.Reason, org.Status.Message)
}

return nil
}
3 changes: 1 addition & 2 deletions api/v1beta1/network_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package v1beta1

import (
"context"
"fmt"

"github.com/pkg/errors"

Expand Down Expand Up @@ -115,7 +114,7 @@ func validateMemberInFederation(ctx context.Context, c client.Client, fedName st
}
for _, m := range members {
if ok := allMembers[m.Name]; !ok {
return fmt.Errorf("allMembers:%#v, members:%#v", allMembers, members)
return errors.Wrapf(errMemberNotInFederation, "allMembers:%#v, members:%#v", allMembers, members)
}
}
return nil
Expand Down
20 changes: 19 additions & 1 deletion api/v1beta1/organization_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import (
)

var (
errHasNetwork = errors.New("the organization is initiator of one network")
errAdminIsEmpty = errors.New("the organization's admin is empty")
errHasNetwork = errors.New("the organization is initiator of one network")
errHasFederation = errors.New("the organization is initiator of one federation")
)

// log is for logging in this package.
Expand Down Expand Up @@ -67,6 +69,9 @@ func (r *Organization) ValidateUpdate(ctx context.Context, client client.Client,
if !isSuperUser(ctx, user) && oldOrg.Spec.Admin != user.Username {
return errNoPermission
}
if r.Spec.Admin == "" {
return errAdminIsEmpty
}
return nil
}

Expand All @@ -76,6 +81,19 @@ func (r *Organization) ValidateDelete(ctx context.Context, c client.Client, user
if !isSuperUser(ctx, user) && r.Spec.Admin != user.Username {
return errNoPermission
}
// validate whether the organization is the initiator of a federation(initiator responsibility)
federationList := &FederationList{}
if err := c.List(ctx, federationList); err != nil {
return errors.Wrap(err, "cant get federation list")
}
for _, fed := range federationList.Items {
for _, m := range fed.Spec.Members {
if m.Initiator && m.Name == r.Name {
return errHasFederation
}
}
}
// validate whether the organization is the initiator of a network(initiator responsibility)
networkList := &NetworkList{}
if err := c.List(ctx, networkList); err != nil {
return errors.Wrap(err, "cant get network list")
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta1/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ func AddWebhooks(mgr ctrl.Manager, setupLog logr.Logger) (err error) {
setupLog.Error(err, "unable to create webhook", "webhook", "Organization")
return err
}
if err = registerCustomWebhook(mgr, &Channel{}, operatorUser); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Channel")
return err
}
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions api/v1beta1/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ var _ = BeforeSuite(func() {
err = registerCustomWebhook(mgr, &Federation{}, fakeOperatorUser)
Expect(err).NotTo(HaveOccurred())

err = registerCustomWebhook(mgr, &Channel{}, fakeOperatorUser)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:webhook

go func() {
Expand Down
2 changes: 1 addition & 1 deletion config/samples/ibp.com_v1beta1_channel_join.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ spec:
- name: org1peer1
namespace: org1
- name: org2peer1
namespace: org1
namespace: org2
description: "channel with org1 & org2"
Loading

0 comments on commit 052eefe

Please sign in to comment.