Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceResolver controller and webhook #325

Merged
merged 1 commit into from
Sep 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package common holds code that isn't tied to a particular CRD version or type.
package common
50 changes: 50 additions & 0 deletions api/common/configentry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package common

import (
"github.com/hashicorp/consul/api"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// ConfigEntryResource is a generic config entry custom resource. It is implemented
// by each config entry type so that they can be acted upon generically.
// It is not tied to a specific CRD version.
type ConfigEntryResource interface {
// GetObjectMeta returns object meta.
GetObjectMeta() metav1.ObjectMeta
// AddFinalizer adds a finalizer to the list of finalizers.
AddFinalizer(name string)
// RemoveFinalizer removes this finalizer from the list.
RemoveFinalizer(name string)
// Finalizers returns the list of finalizers for this object.
Finalizers() []string
// ConsulKind returns the Consul config entry kind, i.e. service-defaults, not
// servicedefaults.
ConsulKind() string
// KubeKind returns the Kube config entry kind, i.e. servicedefaults, not
// service-defaults.
KubeKind() string
// Name returns the name of the config entry.
Name() string
// SetSyncedCondition updates the synced condition.
SetSyncedCondition(status corev1.ConditionStatus, reason, message string)
// SyncedCondition gets the synced condition.
SyncedCondition() (status corev1.ConditionStatus, reason, message string)
// SyncedConditionStatus returns the status of the synced condition.
SyncedConditionStatus() corev1.ConditionStatus
// ToConsul converts the resource to the corresponding Consul API definition.
// Its return type is the generic ConfigEntry but a specific config entry
// type should be constructed e.g. ServiceConfigEntry.
ToConsul() api.ConfigEntry
// MatchesConsul returns true if the resource has the same fields as the Consul
// config entry.
MatchesConsul(candidate api.ConfigEntry) bool
// GetObjectKind should be implemented by the generated code.
GetObjectKind() schema.ObjectKind
ishustava marked this conversation as resolved.
Show resolved Hide resolved
// DeepCopyObject should be implemented by the generated code.
DeepCopyObject() runtime.Object
// Validate returns an error if the resource is invalid.
Validate() error
}
56 changes: 56 additions & 0 deletions api/common/configentry_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package common

import (
"context"
"fmt"
"net/http"

"github.com/go-logr/logr"
ishustava marked this conversation as resolved.
Show resolved Hide resolved
"k8s.io/api/admission/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// ConfigEntryLister is implemented by CRD-specific webhooks.
type ConfigEntryLister interface {
// List returns all resources of this type across all namespaces in a
// Kubernetes cluster.
List(ctx context.Context) ([]ConfigEntryResource, error)
}

// ValidateConfigEntry validates cfgEntry. It is a generic method that
// can be used by all CRD-specific validators.
// Callers should pass themselves as validator and kind should be the custom
// resource name, e.g. "ServiceDefaults".
func ValidateConfigEntry(
ctx context.Context,
req admission.Request,
logger logr.Logger,
configEntryLister ConfigEntryLister,
cfgEntry ConfigEntryResource) admission.Response {

// On create we need to validate that there isn't already a resource with
// the same name in a different namespace since we need to map all Kube
// resources to a single Consul namespace.
if req.Operation == v1beta1.Create {
ishustava marked this conversation as resolved.
Show resolved Hide resolved
logger.Info("validate create", "name", cfgEntry.Name())

list, err := configEntryLister.List(ctx)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
for _, item := range list {
if item.Name() == cfgEntry.Name() {
// todo: If running Consul Ent with mirroring need to change this to respect namespaces.
return admission.Errored(http.StatusBadRequest,
thisisnotashwin marked this conversation as resolved.
Show resolved Hide resolved
fmt.Errorf("%s resource with name %q is already defined – all %s resources must have unique names across namespaces",
cfgEntry.KubeKind(),
cfgEntry.Name(),
cfgEntry.KubeKind()))
}
}
}
if err := cfgEntry.Validate(); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return admission.Allowed(fmt.Sprintf("valid %s request", cfgEntry.KubeKind()))
}
161 changes: 161 additions & 0 deletions api/common/configentry_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package common

import (
"context"
"encoding/json"
"errors"
"testing"

logrtest "github.com/go-logr/logr/testing"
capi "github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

func TestValidateConfigEntry(t *testing.T) {
otherNS := "other"

cases := map[string]struct {
existingResources []ConfigEntryResource
newResource ConfigEntryResource
expAllow bool
expErrMessage string
}{
"no duplicates, valid": {
existingResources: nil,
newResource: &mockConfigEntry{
MockName: "foo",
MockNamespace: otherNS,
Valid: true,
},
expAllow: true,
},
"no duplicates, invalid": {
existingResources: nil,
newResource: &mockConfigEntry{
MockName: "foo",
MockNamespace: otherNS,
Valid: false,
},
expAllow: false,
expErrMessage: "invalid",
},
"duplicate name": {
existingResources: []ConfigEntryResource{&mockConfigEntry{
MockName: "foo",
MockNamespace: "default",
}},
newResource: &mockConfigEntry{
MockName: "foo",
MockNamespace: otherNS,
Valid: true,
},
expAllow: false,
expErrMessage: "mockkind resource with name \"foo\" is already defined – all mockkind resources must have unique names across namespaces",
},
lkysow marked this conversation as resolved.
Show resolved Hide resolved
}
for name, c := range cases {
t.Run(name, func(t *testing.T) {
ctx := context.Background()
marshalledRequestObject, err := json.Marshal(c.newResource)
require.NoError(t, err)

lister := &mockConfigEntryLister{
Resources: c.existingResources,
}
response := ValidateConfigEntry(ctx, admission.Request{
AdmissionRequest: v1beta1.AdmissionRequest{
Name: c.newResource.Name(),
Namespace: otherNS,
Operation: v1beta1.Create,
Object: runtime.RawExtension{
Raw: marshalledRequestObject,
},
},
},
logrtest.TestLogger{T: t},
lister,
c.newResource)
require.Equal(t, c.expAllow, response.Allowed)
if c.expErrMessage != "" {
require.Equal(t, c.expErrMessage, response.AdmissionResponse.Result.Message)
}
})
}
}

type mockConfigEntryLister struct {
Resources []ConfigEntryResource
}

func (in *mockConfigEntryLister) List(_ context.Context) ([]ConfigEntryResource, error) {
return in.Resources, nil
}

type mockConfigEntry struct {
MockName string
MockNamespace string
Valid bool
}

func (in *mockConfigEntry) GetObjectMeta() metav1.ObjectMeta {
return metav1.ObjectMeta{}
}

func (in *mockConfigEntry) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}

func (in *mockConfigEntry) DeepCopyObject() runtime.Object {
return in
}

func (in *mockConfigEntry) AddFinalizer(_ string) {}

func (in *mockConfigEntry) RemoveFinalizer(_ string) {}

func (in *mockConfigEntry) Finalizers() []string {
return nil
}

func (in *mockConfigEntry) ConsulKind() string {
return "mock-kind"
}

func (in *mockConfigEntry) KubeKind() string {
return "mockkind"
}

func (in *mockConfigEntry) Name() string {
return in.MockName
}

func (in *mockConfigEntry) SetSyncedCondition(_ corev1.ConditionStatus, _ string, _ string) {}

func (in *mockConfigEntry) SyncedCondition() (status corev1.ConditionStatus, reason string, message string) {
return corev1.ConditionTrue, "", ""
}

func (in *mockConfigEntry) SyncedConditionStatus() corev1.ConditionStatus {
return corev1.ConditionTrue
}

func (in *mockConfigEntry) ToConsul() capi.ConfigEntry {
return &capi.ServiceConfigEntry{}
}

func (in *mockConfigEntry) Validate() error {
if !in.Valid {
return errors.New("invalid")
}
return nil
}

func (in *mockConfigEntry) MatchesConsul(_ capi.ConfigEntry) bool {
return false
}
2 changes: 2 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

const ConsulHashicorpGroup string = "consul.hashicorp.com"

var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "consul.hashicorp.com", Version: "v1alpha1"}
Expand Down
Loading