Skip to content

Commit

Permalink
Add support for ServiceExports
Browse files Browse the repository at this point in the history
- This PR does not include acceptance tests. The desired acceptance
  tests will be determined based on the expected behavior of
ServiceExports when partitions are disabled.

- Similarly, controller tests have not been added as our existing
  controller tests only account for namespace. The behavior with
partitions has not been resolved.
  • Loading branch information
Ashwin Venkatesh committed Oct 29, 2021
1 parent 1e39777 commit 7cca856
Show file tree
Hide file tree
Showing 15 changed files with 1,190 additions and 0 deletions.
10 changes: 10 additions & 0 deletions acceptance/tests/fixtures/bases/crds-oss/serviceexports.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: consul.hashicorp.com/v1alpha1
kind: ServiceExports
metadata:
name: exports
spec:
services:
- name: frontend
namespace: frontend
consumers:
- partition: other
137 changes: 137 additions & 0 deletions charts/consul/templates/crd-serviceexports.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
{{- if .Values.controller.enabled }}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.6.0
creationTimestamp: null
name: serviceexports.consul.hashicorp.com
labels:
app: {{ template "consul.name" . }}
chart: {{ template "consul.chart" . }}
heritage: {{ .Release.Service }}
release: {{ .Release.Name }}
component: crd
spec:
group: consul.hashicorp.com
names:
kind: ServiceExports
listKind: ServiceExportsList
plural: serviceexports
singular: serviceexports
scope: Namespaced
versions:
- additionalPrinterColumns:
- description: The sync status of the resource with Consul
jsonPath: .status.conditions[?(@.type=="Synced")].status
name: Synced
type: string
- description: The last successful synced time of the resource with Consul
jsonPath: .status.lastSyncedTime
name: Last Synced
type: date
- description: The age of the resource
jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v1alpha1
schema:
openAPIV3Schema:
description: ServiceExports is the Schema for the serviceexports API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ServiceExportsSpec defines the desired state of ServiceExports
properties:
services:
description: Services is a list of services to be exported and the
list of partitions to expose them to.
items:
description: ExportedService manages the exporting of a service
in the local partition to other partitions.
properties:
consumers:
description: Consumers is a list of downstream consumers of
the service to be exported.
items:
description: ServiceConsumer represents a downstream consumer
of the service to be exported.
properties:
partition:
description: Partition is the admin partition to export
the service to.
type: string
type: object
type: array
name:
description: Name is the name of the service to be exported.
type: string
namespace:
description: Namespace is the namespace to export the service
from.
type: string
type: object
type: array
type: object
status:
properties:
conditions:
description: Conditions indicate the latest available observations
of a resource's current state.
items:
description: 'Conditions define a readiness condition for a Consul
resource. See: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties'
properties:
lastTransitionTime:
description: LastTransitionTime is the last time the condition
transitioned from one status to another.
format: date-time
type: string
message:
description: A human readable message indicating details about
the transition.
type: string
reason:
description: The reason for the condition's last transition.
type: string
status:
description: Status of the condition, one of True, False, Unknown.
type: string
type:
description: Type of condition.
type: string
required:
- status
- type
type: object
type: array
lastSyncedTime:
description: LastSyncedTime is the last time the resource successfully
synced with Consul.
format: date-time
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
{{- end }}
24 changes: 24 additions & 0 deletions charts/consul/test/unit/crd-serviceexports.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bats

load _helpers

@test "serviceExports/CustomerResourceDefinition: disabled by default" {
cd `chart_dir`
assert_empty helm template \
-s templates/crd-serviceexports.yaml \
.
}

@test "serviceExports/CustomerResourceDefinition: enabled with controller.enabled=true" {
cd `chart_dir`
local actual=$(helm template \
-s templates/crd-serviceexports.yaml \
--set 'controller.enabled=true' \
. | tee /dev/stderr |
# The generated CRDs have "---" at the top which results in two objects
# being detected by yq, the first of which is null. We must therefore use
# yq -s so that length operates on both objects at once rather than
# individually, which would output false\ntrue and fail the test.
yq -s 'length > 0' | tee /dev/stderr)
[ "${actual}" = "true" ]
}
185 changes: 185 additions & 0 deletions control-plane/api/v1alpha1/serviceexports_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package v1alpha1

import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/consul-k8s/control-plane/api/common"
"github.com/hashicorp/consul/api"
capi "github.com/hashicorp/consul/api"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const ServiceExportsKubeKind = "serviceexports"

func init() {
SchemeBuilder.Register(&ServiceExports{}, &ServiceExportsList{})
}

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

// ServiceExports is the Schema for the serviceexports 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="Last Synced",type="date",JSONPath=".status.lastSyncedTime",description="The last successful synced time of the resource with Consul"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the resource"
type ServiceExports struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

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

//+kubebuilder:object:root=true

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

// ServiceExportsSpec defines the desired state of ServiceExports
type ServiceExportsSpec struct {
// Services is a list of services to be exported and the list of partitions
// to expose them to.
Services []ExportedService `json:"services,omitempty"`
}

// ExportedService manages the exporting of a service in the local partition to
// other partitions.
type ExportedService struct {
// Name is the name of the service to be exported.
Name string `json:"name,omitempty"`

// Namespace is the namespace to export the service from.
Namespace string `json:"namespace,omitempty"`

// Consumers is a list of downstream consumers of the service to be exported.
Consumers []ServiceConsumer `json:"consumers,omitempty"`
}

// ServiceConsumer represents a downstream consumer of the service to be exported.
type ServiceConsumer struct {
// Partition is the admin partition to export the service to.
Partition string `json:"partition,omitempty"`
}

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

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

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

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

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

func (in *ServiceExports) ConsulGlobalResource() bool {
return true
}

func (in *ServiceExports) ConsulMirroringNS() string {
return common.DefaultConsulNamespace
}

func (in *ServiceExports) KubeKind() string {
return ServiceExportsKubeKind
}

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

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

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

func (in *ServiceExports) SetLastSyncedTime(time *metav1.Time) {
in.Status.LastSyncedTime = time
}

func (in *ServiceExports) 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 *ServiceExports) SyncedConditionStatus() corev1.ConditionStatus {
cond := in.Status.GetCondition(ConditionSynced)
if cond == nil {
return corev1.ConditionUnknown
}
return cond.Status
}

func (in *ServiceExports) ToConsul(datacenter string) api.ConfigEntry {
var services []capi.ExportedService
for _, service := range in.Spec.Services {
services = append(services, service.toConsul())
}
return &capi.ServiceExportsConfigEntry{
Services: services,
Meta: meta(datacenter),
}
}

func (in *ExportedService) toConsul() capi.ExportedService {
var consumers []capi.ServiceConsumer
for _, consumer := range in.Consumers {
consumers = append(consumers, capi.ServiceConsumer{Partition: consumer.Partition})
}
return capi.ExportedService{
Name: in.Name,
Namespace: in.Namespace,
Consumers: consumers,
}
}

func (in *ServiceExports) MatchesConsul(candidate api.ConfigEntry) bool {
configEntry, ok := candidate.(*capi.ServiceExportsConfigEntry)
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.ServiceExportsConfigEntry{}, "Partition", "Meta", "ModifyIndex", "CreateIndex"), cmpopts.IgnoreUnexported(), cmpopts.EquateEmpty())

}

func (in *ServiceExports) Validate(_ bool) error {
return nil
}

func (in *ServiceExports) DefaultNamespaceFields(_ bool, _ string, _ bool, _ string) {
}
Loading

0 comments on commit 7cca856

Please sign in to comment.