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 Gatekeeper for managing gateway deployment resources #2117

Merged
merged 38 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b8260f4
Stub out the gatewayclass controller
Apr 14, 2023
d4ac434
Change the controller name
Apr 17, 2023
21a490d
Only register gwv1beta1
Apr 17, 2023
5bd6a65
Address PR feedback
Apr 19, 2023
87da237
Adds stub of Gateway Controller
missylbytes Apr 25, 2023
92725c3
cannot understand why the indexes are not working
missylbytes Apr 26, 2023
3a52d0e
some updates, want to do cleanup
missylbytes Apr 27, 2023
7b926b5
rebase and cleanup
missylbytes Apr 27, 2023
4840c92
Start adding deployer
Apr 26, 2023
c5d88ee
Flesh out tests
Apr 26, 2023
d9c6af1
Refactor into a "gatekeeper"
Apr 28, 2023
4cf2818
Integrate the gatekeeper into the gateway controller
Apr 28, 2023
42f8b92
Simplify the api
May 1, 2023
39a7168
Remove the creation of helm config until later
May 1, 2023
efe63fb
Remove use and rename package to gatekeeper
May 2, 2023
17dfe01
Add labels to apigateway
May 2, 2023
c88cf10
Manage ServiceAccount
May 2, 2023
64e9615
Manage Deployment
May 2, 2023
0b323e0
Add more to deployment
May 2, 2023
4c2f468
Update Helm Values
May 5, 2023
c240329
WIP fleshing out the gateway deployment upsert behavior
May 8, 2023
14e9d9f
Update role and service
May 9, 2023
ecfd8fb
Merge branch 'api-gateways' into NET-3661/gateway-manager
May 9, 2023
09297df
Fix merge conflicts
May 9, 2023
a5ae539
Round out tests
May 10, 2023
7f020a6
Add test for respecting replicas
May 10, 2023
d635633
Change the Gatekeeper New API and add comments for Upsert and Delete
May 11, 2023
c41bb23
implement joinResources
May 11, 2023
1dd3cfc
accept suggestions from @jm96441n
May 11, 2023
76f57c3
Use pointer receivers
May 11, 2023
0794d4d
Separate out mutator
May 11, 2023
d0a13fc
Update deployment correctly
May 12, 2023
16a12f1
Update Role and ServiceAccount
May 12, 2023
bb77bd4
Fix that silly linting error
May 12, 2023
899207c
Comments on HelmConfig
May 12, 2023
93256e6
Add Image to deployment
May 12, 2023
d2aa119
Merge api-gateway into branch
May 15, 2023
dff2519
Merge branch 'api-gateways' into NET-3661/gateway-manager
May 15, 2023
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
85 changes: 85 additions & 0 deletions charts/consul/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1938,6 +1938,90 @@ connectInject:
# @type: integer
minAvailable: null

# Configuration settings for the Consul API Gateway integration.
apiGateway:
# Configuration settings for the optional GatewayClass installed by Consul on Kubernetes.
managedGatewayClass:
# This value defines [`nodeSelector`](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector)
# labels for gateway pod assignment, formatted as a multi-line string.
#
# Example:
#
# ```yaml
# nodeSelector: |
# beta.kubernetes.io/arch: amd64
# ```
#
# @type: string
nodeSelector: null

# Toleration settings for gateway pods created with the managed gateway class.
# This should be a multi-line string matching the
# [Tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) array in a Pod spec.
#
# @type: string
tolerations: null

# This value defines the type of Service created for gateways (e.g. LoadBalancer, ClusterIP)
serviceType: LoadBalancer

# This value toggles if the gateway ports should be mapped to host ports
useHostPorts: false

# Configuration settings for annotations to be copied from the Gateway to other child resources.
copyAnnotations:
# This value defines a list of annotations to be copied from the Gateway to the Service created, formatted as a multi-line string.
#
# Example:
#
# ```yaml
# service:
# annotations: |
# - external-dns.alpha.kubernetes.io/hostname
# ```
#
# @type: string
service: null

# This value defines the number of pods to deploy for each Gateway as well as a min and max number of pods for all Gateways
#
# Example:
#
# ```yaml
# deployment:
# defaultInstances: 3
# maxInstances: 8
# minInstances: 1
# ```
#
# @type: map
deployment: null

# Configuration for the ServiceAccount created for the api-gateway component
serviceAccount:
# This value defines additional annotations for the client service account. This should be formatted as a multi-line
# string.
#
# ```yaml
# annotations: |
# "sample/annotation1": "foo"
# "sample/annotation2": "bar"
# ```
#
# @type: string
annotations: null

# The resource settings for Pods handling traffic for Gateway API.
# @recurse: false
# @type: map
resources:
requests:
memory: "100Mi"
cpu: "100m"
limits:
memory: "100Mi"
cpu: "100m"

# Configures consul-cni plugin for Consul Service mesh services
cni:
# If true, then all traffic redirection setup uses the consul-cni plugin.
Expand Down Expand Up @@ -2867,6 +2951,7 @@ terminatingGateways:
gateways:
- name: terminating-gateway

# [DEPRECATED] Use connectInject.apiGateway instead.
# Configuration settings for the Consul API Gateway integration
apiGateway:
# When true the helm chart will install the Consul API Gateway controller
Expand Down
6 changes: 4 additions & 2 deletions control-plane/api-gateway/controllers/gateway_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/go-logr/logr"
apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -41,8 +42,9 @@ type GatewayControllerConfig struct {
// GatewayController reconciles a Gateway object.
// The Gateway is responsible for defining the behavior of API gateways.
type GatewayController struct {
cache *cache.Cache
Log logr.Logger
HelmConfig apigateway.HelmConfig
Log logr.Logger
cache *cache.Cache
client.Client
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

Expand Down
159 changes: 159 additions & 0 deletions control-plane/api-gateway/gatekeeper/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package gatekeeper

import (
"context"

apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

func (g *Gatekeeper) upsertDeployment(ctx context.Context) error {
// Get Deployment if it exists.
existingDeployment := &appsv1.Deployment{}
exists := false

err := g.Client.Get(ctx, g.namespacedName(), existingDeployment)
if err != nil && !k8serrors.IsNotFound(err) {
return err
} else if k8serrors.IsNotFound(err) {
exists = false
} else {
exists = true
}

deployment := g.deployment()

if exists {
g.Log.Info("Existing Gateway Deployment found.")

// If the user has set the number of replicas, let's respect that.
deployment.Spec.Replicas = existingDeployment.Spec.Replicas
}

mutated := deployment.DeepCopy()
mutator := newDeploymentMutator(deployment, mutated, g.Gateway, g.Client.Scheme())

result, err := controllerutil.CreateOrUpdate(ctx, g.Client, mutated, mutator)
if err != nil {
return err
}

switch result {
case controllerutil.OperationResultCreated:
g.Log.Info("Created Deployment")
case controllerutil.OperationResultUpdated:
g.Log.Info("Updated Deployment")
case controllerutil.OperationResultNone:
g.Log.Info("No change to deployment")
}

return nil
}

func (g *Gatekeeper) deleteDeployment(ctx context.Context) error {
err := g.Client.Delete(ctx, g.deployment())
if k8serrors.IsNotFound(err) {
return nil
}

return err
}

func (g *Gatekeeper) deployment() *appsv1.Deployment {
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: g.Gateway.Name,
Namespace: g.Gateway.Namespace,
Labels: apigateway.LabelsForGateway(&g.Gateway),
Annotations: g.HelmConfig.CopyAnnotations,
},
Spec: appsv1.DeploymentSpec{
Replicas: &g.HelmConfig.Replicas,
Selector: &metav1.LabelSelector{
MatchLabels: apigateway.LabelsForGateway(&g.Gateway),
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: apigateway.LabelsForGateway(&g.Gateway),
Annotations: map[string]string{
"consul.hashicorp.com/connect-inject": "false",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Image: g.HelmConfig.Image,
},
},
Affinity: &corev1.Affinity{
PodAntiAffinity: &corev1.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
{
Weight: 1,
PodAffinityTerm: corev1.PodAffinityTerm{
LabelSelector: &metav1.LabelSelector{
MatchLabels: apigateway.LabelsForGateway(&g.Gateway),
},
TopologyKey: "kubernetes.io/hostname",
},
},
},
},
},
NodeSelector: g.GatewayClassConfig.Spec.NodeSelector, // TODO should I grab this from here or Helm?
Tolerations: g.GatewayClassConfig.Spec.Tolerations,
ServiceAccountName: g.serviceAccountName(),
},
},
},
}
}

func mergeDeployments(a, b *appsv1.Deployment) *appsv1.Deployment {
if !compareDeployments(a, b) {
b.Spec.Template = a.Spec.Template
b.Spec.Replicas = a.Spec.Replicas
}

return b
}

func compareDeployments(a, b *appsv1.Deployment) bool {
// since K8s adds a bunch of defaults when we create a deployment, check that
// they don't differ by the things that we may actually change, namely container
// ports
if len(b.Spec.Template.Spec.Containers) != len(a.Spec.Template.Spec.Containers) {
return false
}
for i, container := range a.Spec.Template.Spec.Containers {
otherPorts := b.Spec.Template.Spec.Containers[i].Ports
if len(container.Ports) != len(otherPorts) {
return false
}
for j, port := range container.Ports {
otherPort := otherPorts[j]
if port.ContainerPort != otherPort.ContainerPort {
return false
}
if port.Protocol != otherPort.Protocol {
return false
}
}
}

return *b.Spec.Replicas == *a.Spec.Replicas
}

func newDeploymentMutator(deployment, mutated *appsv1.Deployment, gateway gwv1beta1.Gateway, scheme *runtime.Scheme) resourceMutator {
return func() error {
mutated = mergeDeployments(deployment, mutated)
return ctrl.SetControllerReference(&gateway, mutated, scheme)
}
}
95 changes: 95 additions & 0 deletions control-plane/api-gateway/gatekeeper/gatekeeper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package gatekeeper

import (
"context"
"fmt"

"github.com/go-logr/logr"
apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway"
"github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
)

// Gatekeeper is used to manage the lifecycle of Gateway deployments and services.
type Gatekeeper struct {
Log logr.Logger
Client client.Client

Gateway gwv1beta1.Gateway
GatewayClassConfig v1alpha1.GatewayClassConfig
HelmConfig apigateway.HelmConfig
}

// New creates a new Gatekeeper from the Config.
func New(log logr.Logger, client client.Client, gateway gwv1beta1.Gateway, gatewayClassConfig v1alpha1.GatewayClassConfig, helmConfig apigateway.HelmConfig) *Gatekeeper {
return &Gatekeeper{
Log: log,
Client: client,
Gateway: gateway,
GatewayClassConfig: gatewayClassConfig,
HelmConfig: helmConfig,
}
}

// Upsert creates or updates the resources for handling routing of network traffic.
func (g *Gatekeeper) Upsert(ctx context.Context) error {
t-eckert marked this conversation as resolved.
Show resolved Hide resolved
g.Log.Info(fmt.Sprintf("Upsert Gateway Deployment %s/%s", g.Gateway.Namespace, g.Gateway.Name))

if err := g.upsertRole(ctx); err != nil {
return err
}

if err := g.upsertServiceAccount(ctx); err != nil {
return err
}

if err := g.upsertService(ctx); err != nil {
return err
}

if err := g.upsertDeployment(ctx); err != nil {
return err
}

return nil
}

// Delete removes the resources for handling routing of network traffic.
func (g *Gatekeeper) Delete(ctx context.Context) error {
if err := g.deleteRole(ctx); err != nil {
return err
}

if err := g.deleteServiceAccount(ctx); err != nil {
return err
}

if err := g.deleteService(ctx); err != nil {
return err
}

if err := g.deleteDeployment(ctx); err != nil {
return err
}

return nil
}

// resourceMutator is passed to create or update functions to mutate Kubernetes resources.
type resourceMutator = func() error

func (g Gatekeeper) namespacedName() types.NamespacedName {
return types.NamespacedName{
Namespace: g.Gateway.Namespace,
Name: g.Gateway.Name,
}
}

func (g Gatekeeper) serviceAccountName() string {
authspecaccount := "" // TODO do I need to add this to GatewayClassConfig?
fmt.Println(authspecaccount)
t-eckert marked this conversation as resolved.
Show resolved Hide resolved

return ""
}
Loading