diff --git a/charts/consul/values.yaml b/charts/consul/values.yaml index 73b805680b..b560a0e3d4 100644 --- a/charts/consul/values.yaml +++ b/charts/consul/values.yaml @@ -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. @@ -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 diff --git a/control-plane/api-gateway/controllers/gateway_controller.go b/control-plane/api-gateway/controllers/gateway_controller.go index 4399623d66..7a022dbab0 100644 --- a/control-plane/api-gateway/controllers/gateway_controller.go +++ b/control-plane/api-gateway/controllers/gateway_controller.go @@ -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" @@ -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 } diff --git a/control-plane/api-gateway/controllers/gateway_controller_test.go b/control-plane/api-gateway/controllers/gateway_controller_test.go index a4baeaa6fb..d6538dfcca 100644 --- a/control-plane/api-gateway/controllers/gateway_controller_test.go +++ b/control-plane/api-gateway/controllers/gateway_controller_test.go @@ -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" ) diff --git a/control-plane/api-gateway/gatekeeper/deployment.go b/control-plane/api-gateway/gatekeeper/deployment.go new file mode 100644 index 0000000000..a1ac2332ab --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/deployment.go @@ -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) + } +} diff --git a/control-plane/api-gateway/gatekeeper/gatekeeper.go b/control-plane/api-gateway/gatekeeper/gatekeeper.go new file mode 100644 index 0000000000..3906a6ab25 --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/gatekeeper.go @@ -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 { + 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) + + return "" +} diff --git a/control-plane/api-gateway/gatekeeper/gatekeeper_test.go b/control-plane/api-gateway/gatekeeper/gatekeeper_test.go new file mode 100644 index 0000000000..bcd78003ca --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/gatekeeper_test.go @@ -0,0 +1,863 @@ +package gatekeeper + +import ( + "context" + "fmt" + "testing" + + logrtest "github.com/go-logr/logr/testr" + apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway" + "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbac "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +var ( + createdAtLabelKey = "gateway.consul.hashicorp.com/created" + createdAtLabelValue = "101010" + name = "test" + namespace = "default" + labels = map[string]string{ + "gateway.consul.hashicorp.com/name": name, + "gateway.consul.hashicorp.com/namespace": namespace, + createdAtLabelKey: createdAtLabelValue, + "gateway.consul.hashicorp.com/managed": "true", + } + listeners = []gwv1beta1.Listener{ + { + Name: "Listener 1", + Port: 8080, + Protocol: "TCP", + }, + { + Name: "Listener 2", + Port: 8081, + Protocol: "UDP", + }, + } +) + +type testCase struct { + gateway gwv1beta1.Gateway + gatewayClassConfig v1alpha1.GatewayClassConfig + helmConfig apigateway.HelmConfig + + initialResources resources + finalResources resources +} + +type resources struct { + deployments []*appsv1.Deployment + roles []*rbac.Role + services []*corev1.Service + serviceAccounts []*corev1.ServiceAccount +} + +func TestUpsert(t *testing.T) { + t.Parallel() + + cases := map[string]testCase{ + "create a new gateway deployment with only Deployment": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + }, + initialResources: resources{}, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{}, + services: []*corev1.Service{}, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + "create a new gateway deployment with managed Service": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + }, + initialResources: resources{}, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{}, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + "create a new gateway deployment with managed Service and ACLs": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + ManageSystemACLs: true, + }, + initialResources: resources{}, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + }, + "update a gateway, adding a listener to a service": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + ManageSystemACLs: true, + }, + initialResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "2"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "2"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + }, + "update a gateway, removing a listener from a service": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: []gwv1beta1.Listener{ + listeners[0], + }, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + ManageSystemACLs: true, + }, + initialResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "2"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + }, "2"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + }, + "updating a gateway deployment respects the number of replicas a user has set": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + }, + initialResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 5, nil, nil, "", "1"), + }, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 5, nil, nil, "", "1"), + }, + roles: []*rbac.Role{}, + services: []*corev1.Service{}, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + s := runtime.NewScheme() + require.NoError(t, gwv1beta1.Install(s)) + require.NoError(t, v1alpha1.AddToScheme(s)) + require.NoError(t, rbac.AddToScheme(s)) + require.NoError(t, corev1.AddToScheme(s)) + require.NoError(t, appsv1.AddToScheme(s)) + + log := logrtest.New(t) + + objs := append(joinResources(tc.initialResources), &tc.gateway, &tc.gatewayClassConfig) + client := fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build() + + gatekeeper := New(log, client, tc.gateway, tc.gatewayClassConfig, tc.helmConfig) + + err := gatekeeper.Upsert(context.Background()) + require.NoError(t, err) + require.NoError(t, validateResourcesExist(t, client, tc.finalResources)) + }) + } +} + +func TestDelete(t *testing.T) { + t.Parallel() + + cases := map[string]testCase{ + "delete a gateway deployment with only Deployment": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + }, + initialResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{}, + roles: []*rbac.Role{}, + services: []*corev1.Service{}, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + "delete a gateway deployment with a managed Service": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + }, + initialResources: resources{ + + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{}, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{}, + roles: []*rbac.Role{}, + services: []*corev1.Service{}, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + "delete a gateway deployment with managed Service and ACLs": { + gateway: gwv1beta1.Gateway{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: gwv1beta1.GatewaySpec{ + Listeners: listeners, + }, + }, + gatewayClassConfig: v1alpha1.GatewayClassConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-gatewayclassconfig", + }, + Spec: v1alpha1.GatewayClassConfigSpec{ + CopyAnnotations: v1alpha1.CopyAnnotationsSpec{}, + ServiceType: (*corev1.ServiceType)(ptrTo("NodePort")), + }, + }, + helmConfig: apigateway.HelmConfig{ + Replicas: 3, + ServiceType: ptrTo("NodePort"), + ManageSystemACLs: true, + }, + initialResources: resources{ + deployments: []*appsv1.Deployment{ + configureDeployment(name, namespace, labels, 3, nil, nil, "", "1"), + }, + roles: []*rbac.Role{ + configureRole(name, namespace, labels, "1"), + }, + services: []*corev1.Service{ + configureService(name, namespace, labels, nil, (corev1.ServiceType)("NodePort"), []corev1.ServicePort{ + { + Name: "Listener 1", + Protocol: "TCP", + Port: 8080, + }, + { + Name: "Listener 2", + Protocol: "UDP", + Port: 8081, + }, + }, "1"), + }, + serviceAccounts: []*corev1.ServiceAccount{ + configureServiceAccount(name, namespace, labels, "1"), + }, + }, + finalResources: resources{ + deployments: []*appsv1.Deployment{}, + roles: []*rbac.Role{}, + services: []*corev1.Service{}, + serviceAccounts: []*corev1.ServiceAccount{}, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + s := runtime.NewScheme() + require.NoError(t, gwv1beta1.Install(s)) + require.NoError(t, v1alpha1.AddToScheme(s)) + require.NoError(t, rbac.AddToScheme(s)) + require.NoError(t, corev1.AddToScheme(s)) + require.NoError(t, appsv1.AddToScheme(s)) + + log := logrtest.New(t) + + objs := append(joinResources(tc.initialResources), &tc.gateway, &tc.gatewayClassConfig) + client := fake.NewClientBuilder().WithScheme(s).WithObjects(objs...).Build() + + gatekeeper := New(log, client, tc.gateway, tc.gatewayClassConfig, tc.helmConfig) + + err := gatekeeper.Delete(context.Background()) + require.NoError(t, err) + require.NoError(t, validateResourcesExist(t, client, tc.finalResources)) + require.NoError(t, validateResourcesAreDeleted(t, client, tc.initialResources)) + }) + } +} + +func joinResources(resources resources) (objs []client.Object) { + for _, deployment := range resources.deployments { + objs = append(objs, deployment) + } + + for _, role := range resources.roles { + objs = append(objs, role) + } + + for _, service := range resources.services { + objs = append(objs, service) + } + + for _, serviceAccount := range resources.serviceAccounts { + objs = append(objs, serviceAccount) + } + + return objs +} + +func validateResourcesExist(t *testing.T, client client.Client, resources resources) error { + for _, expected := range resources.deployments { + actual := &appsv1.Deployment{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if err != nil { + return err + } + + // Patch the createdAt label + actual.Labels[createdAtLabelKey] = createdAtLabelValue + actual.Spec.Selector.MatchLabels[createdAtLabelKey] = createdAtLabelValue + actual.Spec.Template.ObjectMeta.Labels[createdAtLabelKey] = createdAtLabelValue + + require.Equal(t, expected.Name, actual.Name) + require.Equal(t, expected.Namespace, actual.Namespace) + require.Equal(t, expected.APIVersion, actual.APIVersion) + require.Equal(t, expected.Labels, actual.Labels) + require.Equal(t, expected.Spec.Replicas, actual.Spec.Replicas) + } + + for _, expected := range resources.roles { + actual := &rbac.Role{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if err != nil { + return err + } + + // Patch the createdAt label + actual.Labels[createdAtLabelKey] = createdAtLabelValue + + require.Equal(t, expected, actual) + } + + for _, expected := range resources.services { + actual := &corev1.Service{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if err != nil { + return err + } + + // Patch the createdAt label + actual.Labels[createdAtLabelKey] = createdAtLabelValue + actual.Spec.Selector[createdAtLabelKey] = createdAtLabelValue + + require.Equal(t, expected, actual) + } + + for _, expected := range resources.serviceAccounts { + actual := &corev1.ServiceAccount{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if err != nil { + return err + } + + // Patch the createdAt label + actual.Labels[createdAtLabelKey] = createdAtLabelValue + + require.Equal(t, expected, actual) + } + + return nil +} + +func validateResourcesAreDeleted(t *testing.T, client client.Client, resources resources) error { + for _, expected := range resources.deployments { + actual := &appsv1.Deployment{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if !k8serrors.IsNotFound(err) { + return fmt.Errorf("expected deployment %s to be deleted", expected.Name) + } + require.Error(t, err) + } + + for _, expected := range resources.roles { + actual := &rbac.Role{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if !k8serrors.IsNotFound(err) { + return fmt.Errorf("expected role %s to be deleted", expected.Name) + } + require.Error(t, err) + } + + for _, expected := range resources.services { + actual := &corev1.Service{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if !k8serrors.IsNotFound(err) { + return fmt.Errorf("expected service %s to be deleted", expected.Name) + } + require.Error(t, err) + } + + for _, expected := range resources.serviceAccounts { + actual := &corev1.ServiceAccount{} + err := client.Get(context.Background(), types.NamespacedName{ + Name: expected.Name, + Namespace: expected.Namespace, + }, actual) + if !k8serrors.IsNotFound(err) { + return fmt.Errorf("expected service account %s to be deleted", expected.Name) + } + require.Error(t, err) + } + + return nil +} + +func configureDeployment(name, namespace string, labels map[string]string, replicas int32, nodeSelector map[string]string, tolerations []corev1.Toleration, serviceAccoutName, resourceVersion string) *appsv1.Deployment { + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + ResourceVersion: resourceVersion, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "Gateway", + Name: name, + Controller: ptrTo(true), + BlockOwnerDeletion: ptrTo(true), + }, + }, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: map[string]string{ + "consul.hashicorp.com/connect-inject": "false", + }, + }, + Spec: corev1.PodSpec{ + Affinity: &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: 1, + PodAffinityTerm: corev1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + }, + NodeSelector: nodeSelector, + Tolerations: tolerations, + ServiceAccountName: serviceAccoutName, + }, + }, + }, + } +} + +func configureRole(name, namespace string, labels map[string]string, resourceVersion string) *rbac.Role { + return &rbac.Role{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "Role", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + ResourceVersion: resourceVersion, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "Gateway", + Name: name, + Controller: ptrTo(true), + BlockOwnerDeletion: ptrTo(true), + }, + }, + }, + Rules: []rbac.PolicyRule{{ + APIGroups: []string{"policy"}, + Resources: []string{"podsecuritypolicies"}, + Verbs: []string{"use"}, + }}, + } +} + +func configureService(name, namespace string, labels, annotations map[string]string, serviceType corev1.ServiceType, ports []corev1.ServicePort, resourceVersion string) *corev1.Service { + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Service", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + Annotations: annotations, + ResourceVersion: resourceVersion, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "Gateway", + Name: name, + Controller: ptrTo(true), + BlockOwnerDeletion: ptrTo(true), + }, + }, + }, + Spec: corev1.ServiceSpec{ + Selector: labels, + Type: serviceType, + Ports: ports, + }, + } +} + +func configureServiceAccount(name, namespace string, labels map[string]string, resourceVersion string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + ResourceVersion: resourceVersion, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "gateway.networking.k8s.io/v1beta1", + Kind: "Gateway", + Name: name, + Controller: ptrTo(true), + BlockOwnerDeletion: ptrTo(true), + }, + }, + }, + } +} + +func ptrTo[T bool | string](t T) *T { + return &t +} diff --git a/control-plane/api-gateway/gatekeeper/role.go b/control-plane/api-gateway/gatekeeper/role.go new file mode 100644 index 0000000000..85587741fb --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/role.go @@ -0,0 +1,82 @@ +package gatekeeper + +import ( + "context" + "errors" + + apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway" + rbac "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +func (g *Gatekeeper) upsertRole(ctx context.Context) error { + if !g.HelmConfig.ManageSystemACLs { + return nil + } + + // TODO check and do upsert + + role := &rbac.Role{} + exists := false + + // Get ServiceAccount + err := g.Client.Get(ctx, g.namespacedName(), role) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } else if k8serrors.IsNotFound(err) { + exists = false + } else { + exists = true + } + + if exists { + // Ensure we own the Role. + for _, ref := range role.GetOwnerReferences() { + if ref.UID == g.Gateway.GetUID() && ref.Name == g.Gateway.GetName() { + // We found ourselves! + return nil + } + } + return errors.New("Role not owned by controller") + } + + role = g.role() + if err := ctrl.SetControllerReference(&g.Gateway, role, g.Client.Scheme()); err != nil { + return err + } + if err := g.Client.Create(ctx, role); err != nil { + return err + } + + return nil +} + +func (g *Gatekeeper) deleteRole(ctx context.Context) error { + if err := g.Client.Delete(ctx, g.role()); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return err + } + + return nil +} + +func (g *Gatekeeper) role() *rbac.Role { + return &rbac.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: g.Gateway.Name, + Namespace: g.Gateway.Namespace, + Labels: apigateway.LabelsForGateway(&g.Gateway), + }, + Rules: []rbac.PolicyRule{{ + APIGroups: []string{"policy"}, + Resources: []string{"podsecuritypolicies"}, + // TODO figure out how to bring this in. Maybe GWCCFG + // ResourceNames: []string{c.Spec.ConsulSpec.AuthSpec.PodSecurityPolicy}, + Verbs: []string{"use"}, + }}, + } +} diff --git a/control-plane/api-gateway/gatekeeper/service.go b/control-plane/api-gateway/gatekeeper/service.go new file mode 100644 index 0000000000..8bee567387 --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/service.go @@ -0,0 +1,137 @@ +package gatekeeper + +import ( + "context" + + apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + 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" +) + +var ( + defaultServiceAnnotations = []string{ + "external-dns.alpha.kubernetes.io/hostname", + } +) + +func (g *Gatekeeper) upsertService(ctx context.Context) error { + if g.HelmConfig.ServiceType == nil { + return nil + } + + service := g.service() + + mutated := service.DeepCopy() + mutator := newServiceMutator(service, 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 Service") + case controllerutil.OperationResultUpdated: + g.Log.Info("Updated Service") + case controllerutil.OperationResultNone: + g.Log.Info("No change to service") + } + + return nil +} + +func (g *Gatekeeper) deleteService(ctx context.Context) error { + if err := g.Client.Delete(ctx, g.service()); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return err + } + + return nil +} + +func (g *Gatekeeper) service() *corev1.Service { + ports := []corev1.ServicePort{} + for _, listener := range g.Gateway.Spec.Listeners { + ports = append(ports, corev1.ServicePort{ + Name: string(listener.Name), + Protocol: corev1.Protocol(listener.Protocol), + Port: int32(listener.Port), + }) + } + + // Copy annotations from the Gateway, filtered by those allowed by the GatewayClassConfig. + allowedAnnotations := g.GatewayClassConfig.Spec.CopyAnnotations.Service + if allowedAnnotations == nil { + allowedAnnotations = defaultServiceAnnotations + } + annotations := make(map[string]string) + for _, allowedAnnotation := range allowedAnnotations { + if value, found := g.Gateway.Annotations[allowedAnnotation]; found { + annotations[allowedAnnotation] = value + } + } + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: g.Gateway.Name, + Namespace: g.Gateway.Namespace, + Labels: apigateway.LabelsForGateway(&g.Gateway), + Annotations: annotations, + }, + Spec: corev1.ServiceSpec{ + Selector: apigateway.LabelsForGateway(&g.Gateway), + Type: *g.GatewayClassConfig.Spec.ServiceType, + Ports: ports, + }, + } +} + +// mergeService is used to keep annotations and ports from the `from` Service +// to the `to` service. This prevents an infinite reconciliation loop when +// Kubernetes adds this configuration back in. +func mergeService(from, to *corev1.Service) *corev1.Service { + if areServicesEqual(from, to) { + return to + } + + to.Annotations = from.Annotations + to.Spec.Ports = from.Spec.Ports + + return to +} + +func areServicesEqual(a, b *corev1.Service) bool { + if !equality.Semantic.DeepEqual(a.Annotations, b.Annotations) { + return false + } + if len(b.Spec.Ports) != len(a.Spec.Ports) { + return false + } + + for i, port := range a.Spec.Ports { + otherPort := b.Spec.Ports[i] + if port.Port != otherPort.Port { + return false + } + if port.Protocol != otherPort.Protocol { + return false + } + } + return true +} + +func newServiceMutator(service, mutated *corev1.Service, gateway gwv1beta1.Gateway, scheme *runtime.Scheme) resourceMutator { + return func() error { + mutated = mergeService(service, mutated) + return ctrl.SetControllerReference(&gateway, mutated, scheme) + } +} diff --git a/control-plane/api-gateway/gatekeeper/serviceaccount.go b/control-plane/api-gateway/gatekeeper/serviceaccount.go new file mode 100644 index 0000000000..1fe8f44d96 --- /dev/null +++ b/control-plane/api-gateway/gatekeeper/serviceaccount.go @@ -0,0 +1,75 @@ +package gatekeeper + +import ( + "context" + "errors" + + apigateway "github.com/hashicorp/consul-k8s/control-plane/api-gateway" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" +) + +func (g *Gatekeeper) upsertServiceAccount(ctx context.Context) error { + // We don't create the ServiceAccount if we are not using ManagedGatewayClass. + if !g.HelmConfig.ManageSystemACLs { + return nil + } + + serviceAccount := &corev1.ServiceAccount{} + exists := false + + // Get ServiceAccount if it exists. + err := g.Client.Get(ctx, g.namespacedName(), serviceAccount) + if err != nil && !k8serrors.IsNotFound(err) { + return err + } else if k8serrors.IsNotFound(err) { + exists = false + } else { + exists = true + } + + if exists { + // Ensure we own the ServiceAccount. + for _, ref := range serviceAccount.GetOwnerReferences() { + if ref.UID == g.Gateway.GetUID() && ref.Name == g.Gateway.GetName() { + // We found ourselves! + return nil + } + } + return errors.New("ServiceAccount not owned by controller") + } + + // Create the ServiceAccount. + serviceAccount = g.serviceAccount() + if err := ctrl.SetControllerReference(&g.Gateway, serviceAccount, g.Client.Scheme()); err != nil { + return err + } + if err := g.Client.Create(ctx, serviceAccount); err != nil { + return err + } + + return nil +} + +func (g *Gatekeeper) deleteServiceAccount(ctx context.Context) error { + if err := g.Client.Delete(ctx, g.serviceAccount()); err != nil { + if k8serrors.IsNotFound(err) { + return nil + } + return err + } + + return nil +} + +func (g *Gatekeeper) serviceAccount() *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: g.Gateway.Name, + Namespace: g.Gateway.Namespace, + Labels: apigateway.LabelsForGateway(&g.Gateway), + }, + } +} diff --git a/control-plane/api-gateway/helm_config.go b/control-plane/api-gateway/helm_config.go new file mode 100644 index 0000000000..134431683c --- /dev/null +++ b/control-plane/api-gateway/helm_config.go @@ -0,0 +1,25 @@ +package apigateway + +// HelmConfig is the configuration of gateways that comes in from the user's Helm values. +type HelmConfig struct { + // Image is the Consul Dataplane image to use in gateway deployments. + Image string + // Replicas is the number of Pods in a given Deployment of API Gateway for handling requests. + Replicas int32 + // LogLevel is the logging level of the deployed Consul Dataplanes. + LogLevel string + // NodeSelector places Pods in the Deployment on matching Kubernetes Nodes. + NodeSelector map[string]string + // Tolerations place Pods in the Deployment on Kubernetes Nodes by toleration. + Tolerations map[string]string + // ServiceType is the type of service that should be attached to a given Deployment. + ServiceType *string + // CopyAnnotations defines a mapping of annotations to be copied from the Gateway to the Service created. + CopyAnnotations map[string]string + // MaxInstances is the maximum number of replicas in the Deployment of API Gateway for handling requests. + MaxInstances int32 + // MinInstances is the minimum number of replicas in the Deployment of API Gateway for handling requests. + MinInstances int32 + // ManageSystemACLs toggles the behavior of Consul on Kubernetes creating ACLs and RBAC resources for Gateway deployments. + ManageSystemACLs bool +} diff --git a/control-plane/api-gateway/labels.go b/control-plane/api-gateway/labels.go new file mode 100644 index 0000000000..f2d3804f84 --- /dev/null +++ b/control-plane/api-gateway/labels.go @@ -0,0 +1,24 @@ +package apigateway + +import ( + "fmt" + + gwv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" +) + +const ( + nameLabel = "gateway.consul.hashicorp.com/name" + namespaceLabel = "gateway.consul.hashicorp.com/namespace" + createdAtLabel = "gateway.consul.hashicorp.com/created" + managedLabel = "gateway.consul.hashicorp.com/managed" +) + +// LabelsForGateway formats the default labels that appear on objects managed by the controllers. +func LabelsForGateway(gateway *gwv1beta1.Gateway) map[string]string { + return map[string]string{ + nameLabel: gateway.Name, + namespaceLabel: gateway.Namespace, + createdAtLabel: fmt.Sprintf("%d", gateway.CreationTimestamp.Unix()), + managedLabel: "true", + } +}