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

add support of consul namespaces to health checks #376

Merged
merged 15 commits into from
Nov 5, 2020
12 changes: 12 additions & 0 deletions connect-inject/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ const (

// injected is used as the annotation value for annotationInjected
injected = "injected"

// annotationConsulNamespace is the Consul namespace the service is registered into.
annotationConsulNamespace = "consul.hashicorp.com/consul-namespace"
)

var (
Expand Down Expand Up @@ -360,6 +363,15 @@ func (h *Handler) Mutate(req *v1beta1.AdmissionRequest) *v1beta1.AdmissionRespon
labelInject: injected,
})...)

// Consul-ENT only: Add the Consul destination namespace as an annotation to the pod.
if h.EnableNamespaces {
patches = append(patches, updateAnnotation(
pod.Annotations,
map[string]string{
annotationConsulNamespace: h.consulNamespace(req.Namespace),
})...)
}

// Generate the patch
var patch []byte
if len(patches) > 0 {
Expand Down
87 changes: 87 additions & 0 deletions connect-inject/handler_ent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package connectinject

import (
"encoding/json"
"testing"
"time"

Expand All @@ -11,6 +12,7 @@ import (
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/go-hclog"
"github.com/mattbaird/jsonpatch"
"github.com/stretchr/testify/require"
"k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -508,3 +510,88 @@ func TestHandler_MutateWithNamespaces_ACLs(t *testing.T) {
})
}
}

// Test that the annotation for the Consul namespace is added.
func TestHandler_MutateWithNamespaces_Annotation(t *testing.T) {
t.Parallel()
sourceKubeNS := "kube-ns"

cases := map[string]struct {
ConsulDestinationNamespace string
Mirroring bool
MirroringPrefix string
ExpNamespaceAnnotation string
}{
"dest: default": {
ConsulDestinationNamespace: "default",
ExpNamespaceAnnotation: "default",
},
"dest: foo": {
ConsulDestinationNamespace: "foo",
ExpNamespaceAnnotation: "foo",
},
"mirroring": {
Mirroring: true,
ExpNamespaceAnnotation: sourceKubeNS,
},
"mirroring with prefix": {
Mirroring: true,
MirroringPrefix: "prefix-",
ExpNamespaceAnnotation: "prefix-" + sourceKubeNS,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
require := require.New(t)

// Set up consul server
a, err := testutil.NewTestServerConfigT(t, nil)
require.NoError(err)
defer a.Stop()

// Set up consul client
client, err := api.NewClient(&api.Config{
Address: a.HTTPAddr,
})
require.NoError(err)

handler := Handler{
Log: hclog.Default().Named("handler"),
AllowK8sNamespacesSet: mapset.NewSet("*"),
DenyK8sNamespacesSet: mapset.NewSet(),
EnableNamespaces: true,
ConsulDestinationNamespace: c.ConsulDestinationNamespace,
EnableK8SNSMirroring: c.Mirroring,
K8SNSMirroringPrefix: c.MirroringPrefix,
ConsulClient: client,
}

resp := handler.Mutate(&v1beta1.AdmissionRequest{
Object: encodeRaw(t, &corev1.Pod{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "web",
},
},
},
}),
Namespace: sourceKubeNS,
})
require.Equal(resp.Allowed, true)

// Check that the annotation was added as a patch.
var consulNamespaceAnnotationValue string
var patches []jsonpatch.JsonPatchOperation
require.NoError(json.Unmarshal(resp.Patch, &patches))
for _, patch := range patches {
if patch.Path == "/metadata/annotations/"+escapeJSONPointer(annotationConsulNamespace) {
consulNamespaceAnnotationValue = patch.Value.(string)
}
}
require.NotEmpty(consulNamespaceAnnotationValue, "no namespace annotation set")
require.Equal(c.ExpNamespaceAnnotation, consulNamespaceAnnotationValue)
})
}
}
1 change: 1 addition & 0 deletions connect-inject/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ func TestHandlerHandle(t *testing.T) {
},
},
},

{
"pod with existing label",
Handler{
Expand Down
3 changes: 3 additions & 0 deletions connect-inject/health_check_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ func (h *HealthCheckResource) getConsulClient(pod *corev1.Pod) (*api.Client, err
newAddr := fmt.Sprintf("%s://%s:%s", h.ConsulUrl.Scheme, pod.Status.HostIP, h.ConsulUrl.Port())
localConfig := api.DefaultConfig()
localConfig.Address = newAddr
if pod.Annotations[annotationConsulNamespace] != "" {
localConfig.Namespace = pod.Annotations[annotationConsulNamespace]
}
localClient, err := api.NewClient(localConfig)
if err != nil {
h.Log.Error("unable to get Consul API Client", "addr", newAddr, "err", err)
Expand Down
242 changes: 242 additions & 0 deletions connect-inject/health_check_resource_ent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// +build enterprise
lkysow marked this conversation as resolved.
Show resolved Hide resolved

package connectinject

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
testNamespace = "testnamespace"
testNamespacedHealthCheckID = "testnamespace_test-pod-test-service_kubernetes-health-check-ttl"
)

var ignoredFieldsEnterprise = []string{"Node", "Definition", "ServiceID", "ServiceName"}

var testPodWithNamespace = corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: testPodName,
},
Spec: corev1.PodSpec{},
}

func TestReconcilePodWithNamespace(t *testing.T) {
kschoche marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()
cases := []struct {
Name string
PreCreateHealthCheck bool
InitialState string
Pod *corev1.Pod
Expected *api.AgentCheck
}{
{
Name: "reconcilePod will create check and set passed",
PreCreateHealthCheck: false,
InitialState: "", // only used when precreating a health check
Pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
kschoche marked this conversation as resolved.
Show resolved Hide resolved
Labels: map[string]string{labelInject: "true"},
Annotations: map[string]string{
annotationStatus: injected,
annotationService: testServiceNameAnnotation,
annotationConsulNamespace: testNamespace,
},
},
Spec: testPodSpec,
Status: corev1.PodStatus{
HostIP: "127.0.0.1",
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
}},
},
},
Expected: &api.AgentCheck{
CheckID: testNamespacedHealthCheckID,
Status: api.HealthPassing,
Notes: "",
Output: kubernetesSuccessReasonMsg,
Type: ttl,
Name: name,
Namespace: testNamespace,
},
},
{
Name: "reconcilePod will create check and set failed",
PreCreateHealthCheck: false,
InitialState: "", // only used when precreating a health check
Pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Labels: map[string]string{labelInject: "true"},
Annotations: map[string]string{
annotationStatus: injected,
annotationService: testServiceNameAnnotation,
annotationConsulNamespace: testNamespace,
},
},
Spec: testPodSpec,
Status: corev1.PodStatus{
HostIP: "127.0.0.1",
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{{
Type: corev1.PodReady,
Status: corev1.ConditionFalse,
Message: testFailureMessage,
}},
},
},
Expected: &api.AgentCheck{
CheckID: testNamespacedHealthCheckID,
Status: api.HealthCritical,
Notes: "",
Output: testFailureMessage,
Type: ttl,
Name: name,
Namespace: testNamespace,
},
},
{
Name: "precreate a passing pod and change to failed",
PreCreateHealthCheck: true,
InitialState: api.HealthPassing,
Pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Labels: map[string]string{labelInject: "true"},
Annotations: map[string]string{
annotationStatus: injected,
annotationService: testServiceNameAnnotation,
annotationConsulNamespace: testNamespace,
},
},
Spec: testPodSpec,
Status: corev1.PodStatus{
HostIP: "127.0.0.1",
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{{
Type: corev1.PodReady,
Status: corev1.ConditionFalse,
Message: testFailureMessage,
}},
},
},
Expected: &api.AgentCheck{
CheckID: testNamespacedHealthCheckID,
Status: api.HealthCritical,
Output: testFailureMessage,
Type: ttl,
Name: name,
Namespace: testNamespace,
},
},
{
Name: "precreate failed pod and change to passing",
PreCreateHealthCheck: true,
InitialState: api.HealthCritical,
Pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Labels: map[string]string{labelInject: "true"},
Annotations: map[string]string{
annotationStatus: injected,
annotationService: testServiceNameAnnotation,
annotationConsulNamespace: testNamespace,
},
},
Spec: testPodSpec,
Status: corev1.PodStatus{
HostIP: "127.0.0.1",
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
}},
},
},
Expected: &api.AgentCheck{
CheckID: testNamespacedHealthCheckID,
Status: api.HealthPassing,
Output: testCheckNotesPassing,
Type: ttl,
Name: name,
Namespace: testNamespace,
},
},
{
Name: "precreate failed check, no pod changes results in no health check changes",
kschoche marked this conversation as resolved.
Show resolved Hide resolved
PreCreateHealthCheck: true,
InitialState: api.HealthCritical,
Pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: testPodName,
Namespace: testNamespace,
Labels: map[string]string{labelInject: "true"},
Annotations: map[string]string{
annotationStatus: injected,
annotationService: testServiceNameAnnotation,
annotationConsulNamespace: testNamespace,
},
},
Spec: testPodSpec,
Status: corev1.PodStatus{
HostIP: "127.0.0.1",
Phase: corev1.PodRunning,
Conditions: []corev1.PodCondition{{
Type: corev1.PodReady,
Status: corev1.ConditionFalse,
}},
},
},
Expected: &api.AgentCheck{
CheckID: testNamespacedHealthCheckID,
Status: api.HealthCritical,
Output: "", // when there is no change in status, Consul doesnt set the Output field
Type: ttl,
Name: name,
Namespace: testNamespace,
},
},
}
for _, tt := range cases {
t.Run(tt.Name, func(t *testing.T) {
require := require.New(t)
// Get a server, client, and handler.
server, client, resource := testServerAgentResourceAndControllerWithConsulNS(t, tt.Pod, testNamespace)
defer server.Stop()
// Register the service with Consul.
kschoche marked this conversation as resolved.
Show resolved Hide resolved
_, _, err := client.Namespaces().Create(&api.Namespace{Name: testNamespace}, nil)
require.NoError(err)
err = client.Agent().ServiceRegister(&api.AgentServiceRegistration{
kschoche marked this conversation as resolved.
Show resolved Hide resolved
ID: testServiceNameReg,
Name: testServiceNameAnnotation,
Namespace: testNamespace,
})
require.NoError(err)
if tt.PreCreateHealthCheck {
// Register the health check if this is not an object create path.
registerHealthCheck(t, client, tt.InitialState)
}
// Upsert and Reconcile both use reconcilePod to reconcile a pod.
err = resource.reconcilePod(tt.Pod)
require.NoError(err)
// Get the agent checks if they were registered.
actual := getConsulAgentChecks(t, client, testNamespacedHealthCheckID)
require.True(cmp.Equal(actual, tt.Expected, cmpopts.IgnoreFields(api.AgentCheck{}, ignoredFieldsEnterprise...)))
})
}
}
Loading