Skip to content

Commit

Permalink
TerminatingGateway CRD
Browse files Browse the repository at this point in the history
  • Loading branch information
lkysow committed Nov 27, 2020
1 parent 8e4a54a commit 184c83d
Show file tree
Hide file tree
Showing 19 changed files with 1,322 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

FEATURES:
* CRDs: add new CRD `IngressGateway` for configuring Consul's [ingress-gateway](https://www.consul.io/docs/agent/config-entries/ingress-gateway) config entry. [[GH-407](https://github.com/hashicorp/consul-k8s/pull/407)]
* CRDs: add new CRD `TerminatingGateway` for configuring Consul's [terminating-gateway](https://www.consul.io/docs/agent/config-entries/terminating-gateway) config entry. [[GH-408](https://github.com/hashicorp/consul-k8s/pull/408)]

## 0.21.0 (November 25, 2020)

Expand Down
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ resources:
- group: consul
kind: ServiceSplitter
version: v1alpha1
- group: consul
kind: TerminatingGateway
version: v1alpha1
version: 3-alpha
plugins:
go.operator-sdk.io/v2-alpha: {}
15 changes: 8 additions & 7 deletions api/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
package common

const (
ServiceDefaults string = "servicedefaults"
ProxyDefaults string = "proxydefaults"
ServiceResolver string = "serviceresolver"
ServiceRouter string = "servicerouter"
ServiceSplitter string = "servicesplitter"
ServiceIntentions string = "serviceintentions"
IngressGateway string = "ingressgateway"
ServiceDefaults string = "servicedefaults"
ProxyDefaults string = "proxydefaults"
ServiceResolver string = "serviceresolver"
ServiceRouter string = "servicerouter"
ServiceSplitter string = "servicesplitter"
ServiceIntentions string = "serviceintentions"
IngressGateway string = "ingressgateway"
TerminatingGateway string = "terminatinggateway"

Global string = "global"
DefaultConsulNamespace string = "default"
Expand Down
225 changes: 225 additions & 0 deletions api/v1alpha1/terminatinggateway_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package v1alpha1

import (
"encoding/json"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
capi "github.com/hashicorp/consul/api"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
)

const (
terminatingGatewayKubeKind = "terminatinggateway"
)

func init() {
SchemeBuilder.Register(&TerminatingGateway{}, &TerminatingGatewayList{})
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// TerminatingGateway is the Schema for the terminatinggateways API
// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type==\"Synced\")].status",description="The sync status of the resource with Consul"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource"
type TerminatingGateway struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec TerminatingGatewaySpec `json:"spec,omitempty"`
Status `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// TerminatingGatewayList contains a list of TerminatingGateway
type TerminatingGatewayList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []TerminatingGateway `json:"items"`
}

// TerminatingGatewaySpec defines the desired state of TerminatingGateway
type TerminatingGatewaySpec struct {
// Services is a list of service names represented by the terminating gateway.
Services []LinkedService `json:"services,omitempty"`
}

// A LinkedService is a service represented by a terminating gateway
type LinkedService struct {
// The namespace the service is registered in.
Namespace string `json:"namespace,omitempty"`

// Name is the name of the service, as defined in Consul's catalog.
Name string `json:"name,omitempty"`

// CAFile is the optional path to a CA certificate to use for TLS connections
// from the gateway to the linked service.
CAFile string `json:"caFile,omitempty"`

// CertFile is the optional path to a client certificate to use for TLS connections
// from the gateway to the linked service.
CertFile string `json:"certFile,omitempty"`

// KeyFile is the optional path to a private key to use for TLS connections
// from the gateway to the linked service.
KeyFile string `json:"keyFile,omitempty"`

// SNI is the optional name to specify during the TLS handshake with a linked service.
SNI string `json:"sni,omitempty"`
}

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

func (in *TerminatingGateway) AddFinalizer(name string) {
in.ObjectMeta.Finalizers = append(in.Finalizers(), name)
}

func (in *TerminatingGateway) RemoveFinalizer(name string) {
var newFinalizers []string
for _, oldF := range in.Finalizers() {
if oldF != name {
newFinalizers = append(newFinalizers, oldF)
}
}
in.ObjectMeta.Finalizers = newFinalizers
}

func (in *TerminatingGateway) Finalizers() []string {
return in.ObjectMeta.Finalizers
}

func (in *TerminatingGateway) ConsulKind() string {
return capi.TerminatingGateway
}

func (in *TerminatingGateway) ConsulGlobalResource() bool {
return false
}

func (in *TerminatingGateway) ConsulMirroringNS() string {
return in.Namespace
}

func (in *TerminatingGateway) KubeKind() string {
return terminatingGatewayKubeKind
}

func (in *TerminatingGateway) ConsulName() string {
return in.ObjectMeta.Name
}

func (in *TerminatingGateway) KubernetesName() string {
return in.ObjectMeta.Name
}

func (in *TerminatingGateway) SetSyncedCondition(status corev1.ConditionStatus, reason, message string) {
in.Status.Conditions = Conditions{
{
Type: ConditionSynced,
Status: status,
LastTransitionTime: metav1.Now(),
Reason: reason,
Message: message,
},
}
}

func (in *TerminatingGateway) SyncedCondition() (status corev1.ConditionStatus, reason, message string) {
cond := in.Status.GetCondition(ConditionSynced)
if cond == nil {
return corev1.ConditionUnknown, "", ""
}
return cond.Status, cond.Reason, cond.Message
}

func (in *TerminatingGateway) SyncedConditionStatus() corev1.ConditionStatus {
condition := in.Status.GetCondition(ConditionSynced)
if condition == nil {
return corev1.ConditionUnknown
}
return condition.Status
}

func (in *TerminatingGateway) ToConsul(datacenter string) capi.ConfigEntry {
var svcs []capi.LinkedService
for _, s := range in.Spec.Services {
svcs = append(svcs, s.toConsul())
}
return &capi.TerminatingGatewayConfigEntry{
Kind: in.ConsulKind(),
Name: in.ConsulName(),
Services: svcs,
Meta: meta(datacenter),
}
}

func (in *TerminatingGateway) MatchesConsul(candidate capi.ConfigEntry) bool {
configEntry, ok := candidate.(*capi.TerminatingGatewayConfigEntry)
if !ok {
return false
}
// No datacenter is passed to ToConsul as we ignore the Meta field when checking for equality.
return cmp.Equal(in.ToConsul(""), configEntry, cmpopts.IgnoreFields(capi.TerminatingGatewayConfigEntry{}, "Namespace", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty())
}

func (in *TerminatingGateway) Validate(namespacesEnabled bool) error {
var errs field.ErrorList
path := field.NewPath("spec")

for i, v := range in.Spec.Services {
errs = append(errs, v.validate(path.Child("services").Index(i))...)
}

errs = append(errs, in.validateNamespaces(namespacesEnabled)...)

if len(errs) > 0 {
return apierrors.NewInvalid(
schema.GroupKind{Group: ConsulHashicorpGroup, Kind: terminatingGatewayKubeKind},
in.KubernetesName(), errs)
}
return nil
}

func (in LinkedService) toConsul() capi.LinkedService {
return capi.LinkedService{
Namespace: in.Namespace,
Name: in.Name,
CAFile: in.CAFile,
CertFile: in.CertFile,
KeyFile: in.KeyFile,
SNI: in.SNI,
}
}

func (in LinkedService) validate(path *field.Path) field.ErrorList {
var errs field.ErrorList
if (in.CertFile != "" && in.KeyFile == "") || (in.KeyFile != "" && in.CertFile == "") {
asJSON, _ := json.Marshal(in)
errs = append(errs, field.Invalid(path,
string(asJSON),
"if certFile or keyFile is set, the other must also be set"))
}
return errs
}

func (in *TerminatingGateway) validateNamespaces(namespacesEnabled bool) field.ErrorList {
var errs field.ErrorList
path := field.NewPath("spec")
if !namespacesEnabled {
for i, service := range in.Spec.Services {
if service.Namespace != "" {
errs = append(errs, field.Invalid(path.Child("services").Index(i).Child("namespace"),
service.Namespace, `Consul Enterprise namespaces must be enabled to set service.namespace`))
}
}
}
return errs
}
Loading

0 comments on commit 184c83d

Please sign in to comment.