Skip to content

Commit

Permalink
Add webhook validations secret object
Browse files Browse the repository at this point in the history
- Update of secrets are allowed only when the secret key
  (which is reffered by CPA) is not changed.

- Delete of secrets are allowed only when it is not being
  referred by CPA.

Signed-off-by: Anand Kumar <[email protected]>
  • Loading branch information
Anandkumar26 committed Sep 8, 2022
1 parent fe6d264 commit dc92bd2
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ DOCKER_SRC=/usr/src/antrea.io/nephe
DOCKER_GOPATH=/tmp/gopath
DOCKER_GOCACHE=/tmp/gocache
GENERATE_CODE_LIST={$$(go list ./... | grep -e apis/crd -e apis/runtime | paste -s -d, -)}
GENERATE_MANIFEST_LIST={$$(go list ./... | grep apis/crd | paste -s -d, -)}
GENERATE_MANIFEST_LIST={$$(go list ./... | grep -E 'apis/crd|pkg/apiserver/webhook' | paste -s -d, -)}

DOCKERIZE := \
docker run --rm -u $$(id -u):$$(id -g) \
Expand Down
7 changes: 7 additions & 0 deletions cmd/nephe-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"

antreanetworking "antrea.io/antrea/pkg/apis/controlplane/v1beta2"
antreatypes "antrea.io/antrea/pkg/apis/crd/v1alpha2"
crdv1alpha1 "antrea.io/nephe/apis/crd/v1alpha1"
runtimev1alpha1 "antrea.io/nephe/apis/runtime/v1alpha1"
"antrea.io/nephe/pkg/apiserver"
nephewebhook "antrea.io/nephe/pkg/apiserver/webhook"
controllers "antrea.io/nephe/pkg/controllers/cloud"
"antrea.io/nephe/pkg/logging"
// +kubebuilder:scaffold:imports
Expand Down Expand Up @@ -135,6 +137,11 @@ func main() {
os.Exit(1)
}

// Register webhook for secret
mgr.GetWebhookServer().Register("/validate-v1-secret",
&webhook.Admission{Handler: &nephewebhook.SecretValidator{Client: mgr.GetClient(),
Log: logging.GetLogger("webhook").WithName("Secret")}})

if err = (&apiserver.NepheControllerAPIServer{}).SetupWithManager(mgr,
npController.GetVirtualMachinePolicyIndexer(), logging.GetLogger("apiServer")); err != nil {
setupLog.Error(err, "unable to create APIServer")
Expand Down
22 changes: 22 additions & 0 deletions config/nephe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -901,3 +901,25 @@ webhooks:
resources:
- cloudprovideraccounts
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
caBundle: Cg==
service:
name: nephe-controller-webhook-service
namespace: nephe-system
path: /validate-v1-secret
failurePolicy: Fail
name: vsecret.kb.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- secrets
sideEffects: None
20 changes: 20 additions & 0 deletions config/webhook/manifests-new.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,23 @@ webhooks:
- cloudprovideraccounts
sideEffects: None
admissionReviewVersions: ["v1", "v1beta1"]
- clientConfig:
caBundle: Cg==
service:
name: nephe-controller-webhook-service
namespace: system
path: /validate-v1-secret
failurePolicy: Fail
name: vsecret.kb.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- secrets
sideEffects: None
admissionReviewVersions: ["v1", "v1beta1"]
21 changes: 21 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,24 @@ webhooks:
resources:
- virtualmachines
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-v1-secret
failurePolicy: Fail
name: vsecret.kb.io
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- UPDATE
- DELETE
resources:
- secrets
sideEffects: None
159 changes: 159 additions & 0 deletions pkg/apiserver/webhook/secret_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2022 Antrea 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"
"encoding/json"
"fmt"
"net/http"

"github.com/go-logr/logr"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
controllerclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

crdv1alpha1 "antrea.io/nephe/apis/crd/v1alpha1"
)

// nolint:lll
// +kubebuilder:webhook:verbs=update;delete,path=/validate-v1-secret,mutating=false,failurePolicy=fail,groups="",resources=secrets,versions=v1,name=vsecret.kb.io,sideEffects=None,admissionReviewVersions=v1;v1beta1

// SecretValidator is used to validate Secret.
type SecretValidator struct {
Client controllerclient.Client
Log logr.Logger
decoder *admission.Decoder
}

// Handle handles admission requests for Secret.
func (v *SecretValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
v.Log.V(1).Info("Received admission webhook", "req", req, "operation", req.Operation)
switch req.Operation {
case admissionv1.Create:
return v.validateCreate(req)
case admissionv1.Update:
return v.validateUpdate(req)
case admissionv1.Delete:
return v.validateDelete(req)
default:
return admission.Errored(http.StatusBadRequest, fmt.Errorf("invalid admission webhook request"))
}
}

// InjectDecoder injects the decoder.
func (v *SecretValidator) InjectDecoder(d *admission.Decoder) error { //nolint:unparam
v.decoder = d
return nil
}

// allowSecretUpdate returns true only when Secret data key is unchanged.
func (v *SecretValidator) allowSecretUpdate(new *corev1.Secret, old *corev1.Secret, key string) bool {
if changed := string(new.Data[key]) != string(old.Data[key]); changed {
return false
}
return true
}

// getCPABySecret returns nil only when the Secret is not used by any CloudProvideAccount CR,
// otherwise the dependent CloudProvideAccount CR will be returned.
func (v *SecretValidator) getCPABySecret(s *corev1.Secret) (error, *crdv1alpha1.CloudProviderAccount) {
cpaList := &crdv1alpha1.CloudProviderAccountList{}
err := v.Client.List(context.TODO(), cpaList, &controllerclient.ListOptions{})
if err != nil {
return fmt.Errorf("failed to get CloudProviderAccount list, err:%v", err), nil
}

for _, cpa := range cpaList.Items {
v.Log.V(1).Info("Checking CloudProvideAccount", "Name", cpa.Name,
"Namespace", cpa.Namespace)
if cpa.Spec.AWSConfig != nil {
if cpa.Spec.AWSConfig.SecretRef.Name == s.Name &&
cpa.Spec.AWSConfig.SecretRef.Namespace == s.Namespace {
return nil, &cpa
}
}

if cpa.Spec.AzureConfig != nil {
if cpa.Spec.AzureConfig.SecretRef.Name == s.Name &&
cpa.Spec.AzureConfig.SecretRef.Namespace == s.Namespace {
return nil, &cpa
}
}
}
return nil, nil
}

func (v *SecretValidator) validateCreate(req admission.Request) admission.Response { // nolint: unparam
return admission.Allowed("")
}

func (v *SecretValidator) validateUpdate(req admission.Request) admission.Response {
newSecret := &corev1.Secret{}
err := v.decoder.Decode(req, newSecret)
if err != nil {
v.Log.Error(err, "Failed to decode Secret", "SecretValidator", req.Name)
return admission.Errored(http.StatusBadRequest, err)
}
oldSecret := &corev1.Secret{}
if req.OldObject.Raw != nil {
if err := json.Unmarshal(req.OldObject.Raw, &oldSecret); err != nil {
v.Log.Error(err, "Failed to decode old Secret", "SecretValidator", req.Name)
return admission.Errored(http.StatusBadRequest, err)
}
}

err, cpa := v.getCPABySecret(oldSecret)
if err != nil {
return admission.Denied(err.Error())
}
if cpa != nil {
var key string
if cpa.Spec.AWSConfig != nil {
key = cpa.Spec.AWSConfig.SecretRef.Key
} else if cpa.Spec.AzureConfig != nil {
key = cpa.Spec.AzureConfig.SecretRef.Key
}
if ok := v.allowSecretUpdate(newSecret, oldSecret, key); !ok {
v.Log.Error(nil, "The Secret is referred by a CloudProviderAccount. Cannot modify it,",
"Secret", oldSecret.Name, "CloudProviderAccount", cpa.Name)
return admission.Denied(fmt.Sprintf("the Secret %v is referred by a "+
"CloudProviderAccount %s. The %s field 'value' cannot be changed", oldSecret.Name, cpa.Name, key))
}
}
return admission.Allowed("")
}

func (v *SecretValidator) validateDelete(req admission.Request) admission.Response {
oldSecret := &corev1.Secret{}
if req.OldObject.Raw != nil {
if err := json.Unmarshal(req.OldObject.Raw, &oldSecret); err != nil {
v.Log.Error(err, "Failed to decode old Secret", "SecretValidator", req.Name)
return admission.Errored(http.StatusBadRequest, err)
}
}
err, cpa := v.getCPABySecret(oldSecret)
if err != nil {
return admission.Denied(err.Error())
}
if cpa != nil {
v.Log.Error(nil, "The Secret is referred by a CloudProviderAccount. Cannot delete it,",
"Secret", oldSecret.Name, "CloudProviderAccount", cpa.Name)
return admission.Denied(fmt.Sprintf("the Secret %s is referred by a CloudProviderAccount %s. "+
"Please delete the CloudProviderAccount first", oldSecret.Name, cpa.Name))
}
return admission.Allowed("")
}

0 comments on commit dc92bd2

Please sign in to comment.