-
Notifications
You must be signed in to change notification settings - Fork 323
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ServiceResolver controller and webhook
* Also make controller and webhook code generic * Update to controller-runtime 0.6.3 to fix spurious log error message.
- Loading branch information
Showing
34 changed files
with
2,743 additions
and
761 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
// DeepCopyObject should be implemented by the generated code. | ||
DeepCopyObject() runtime.Object | ||
// Validate returns an error if the resource is invalid. | ||
Validate() error | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
"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 { | ||
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, | ||
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())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
} | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.