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

TerminatingGateway CRD #408

Merged
merged 1 commit into from
Dec 17, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ BREAKING CHANGES:

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