diff --git a/charts/consul/templates/connect-inject-clusterrole.yaml b/charts/consul/templates/connect-inject-clusterrole.yaml index 08935cff39..00e2e23a50 100644 --- a/charts/consul/templates/connect-inject-clusterrole.yaml +++ b/charts/consul/templates/connect-inject-clusterrole.yaml @@ -70,6 +70,24 @@ rules: - get - patch - update +- apiGroups: ["consul.hashicorp.com"] + resources: ["peeringdialers"] + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - consul.hashicorp.com + resources: + - peeringdialers/status + verbs: + - get + - patch + - update {{- if .Values.global.enablePodSecurityPolicies }} - apiGroups: [ "policy" ] resources: [ "podsecuritypolicies" ] diff --git a/charts/consul/templates/crd-peeringacceptors.yaml b/charts/consul/templates/crd-peeringacceptors.yaml index c938375099..cc7d358319 100644 --- a/charts/consul/templates/crd-peeringacceptors.yaml +++ b/charts/consul/templates/crd-peeringacceptors.yaml @@ -85,8 +85,7 @@ spec: type: string type: object secret: - description: Secret shows any errors during the last reconciliation - of this resource. + description: SecretRef shows the status of the secret. properties: backend: description: 'Backend is where the generated secret is stored. @@ -96,7 +95,7 @@ spec: description: Key is the key of the secret generated. type: string latestHash: - description: LatestHash is the SHA256 sum of the secret generated. + description: ResourceVersion is the resource version for the secret. type: string name: description: Name is the name of the secret generated. diff --git a/charts/consul/templates/crd-peeringdialers.yaml b/charts/consul/templates/crd-peeringdialers.yaml new file mode 100644 index 0000000000..150c44f38c --- /dev/null +++ b/charts/consul/templates/crd-peeringdialers.yaml @@ -0,0 +1,116 @@ +{{- if .Values.connectInject.enabled }} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: peeringdialers.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: PeeringDialer + listKind: PeeringDialerList + plural: peeringdialers + singular: peeringdialer + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: PeeringDialer is the Schema for the peeringdialers 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: PeeringDialerSpec defines the desired state of PeeringDialer. + properties: + peer: + description: Peer describes the information needed to create a peering. + properties: + secret: + description: Secret describes how to store the generated peering + token. + properties: + backend: + description: 'Backend is where the generated secret is stored. + Currently supports the value: "kubernetes".' + type: string + key: + description: Key is the key of the secret generated. + type: string + name: + description: Name is the name of the secret generated. + type: string + type: object + type: object + required: + - peer + type: object + status: + description: PeeringDialerStatus defines the observed state of PeeringDialer. + properties: + lastReconcileTime: + description: LastReconcileTime is the last time the resource was reconciled. + format: date-time + type: string + reconcileError: + description: ReconcileError shows any errors during the last reconciliation + of this resource. + properties: + error: + description: Error is a boolean indicating if there was an error + during the last reconcile of this resource. + type: boolean + message: + description: Message displays the error message from the last + reconcile. + type: string + type: object + secret: + description: SecretRef shows the status of the secret. + properties: + backend: + description: 'Backend is where the generated secret is stored. + Currently supports the value: "kubernetes".' + type: string + key: + description: Key is the key of the secret generated. + type: string + latestHash: + description: ResourceVersion is the resource version for the secret. + type: string + name: + description: Name is the name of the secret generated. + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +{{- end }} diff --git a/control-plane/PROJECT b/control-plane/PROJECT index cb73b0b816..c11e857849 100644 --- a/control-plane/PROJECT +++ b/control-plane/PROJECT @@ -68,4 +68,13 @@ resources: kind: PeeringAcceptor path: github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: hashicorp.com + group: consul + kind: PeeringDialer + path: github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/control-plane/api/v1alpha1/peeringdialer_types.go b/control-plane/api/v1alpha1/peeringdialer_types.go new file mode 100644 index 0000000000..f6eaafe839 --- /dev/null +++ b/control-plane/api/v1alpha1/peeringdialer_types.go @@ -0,0 +1,55 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +func init() { + SchemeBuilder.Register(&PeeringDialer{}, &PeeringDialerList{}) +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// PeeringDialer is the Schema for the peeringdialers API. +type PeeringDialer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PeeringDialerSpec `json:"spec,omitempty"` + Status PeeringDialerStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// PeeringDialerList contains a list of PeeringDialer. +type PeeringDialerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PeeringDialer `json:"items"` +} + +// PeeringDialerSpec defines the desired state of PeeringDialer. +type PeeringDialerSpec struct { + // Important: Run "make" to regenerate code after modifying this file + + // Peer describes the information needed to create a peering. + Peer *Peer `json:"peer"` +} + +// PeeringDialerStatus defines the observed state of PeeringDialer. +type PeeringDialerStatus struct { + // Important: Run "make" to regenerate code after modifying this file + + // LastReconcileTime is the last time the resource was reconciled. + // +optional + LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty" description:"last time the resource was reconciled"` + // ReconcileError shows any errors during the last reconciliation of this resource. + // +optional + ReconcileError *ReconcileErrorStatus `json:"reconcileError,omitempty"` + // SecretRef shows the status of the secret. + // +optional + SecretRef *SecretRefStatus `json:"secret,omitempty"` +} diff --git a/control-plane/api/v1alpha1/zz_generated.deepcopy.go b/control-plane/api/v1alpha1/zz_generated.deepcopy.go index cc10cb4af1..be74e6b131 100644 --- a/control-plane/api/v1alpha1/zz_generated.deepcopy.go +++ b/control-plane/api/v1alpha1/zz_generated.deepcopy.go @@ -942,6 +942,114 @@ func (in *PeeringAcceptorStatus) DeepCopy() *PeeringAcceptorStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PeeringDialer) DeepCopyInto(out *PeeringDialer) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeeringDialer. +func (in *PeeringDialer) DeepCopy() *PeeringDialer { + if in == nil { + return nil + } + out := new(PeeringDialer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PeeringDialer) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PeeringDialerList) DeepCopyInto(out *PeeringDialerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PeeringDialer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeeringDialerList. +func (in *PeeringDialerList) DeepCopy() *PeeringDialerList { + if in == nil { + return nil + } + out := new(PeeringDialerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PeeringDialerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PeeringDialerSpec) DeepCopyInto(out *PeeringDialerSpec) { + *out = *in + if in.Peer != nil { + in, out := &in.Peer, &out.Peer + *out = new(Peer) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeeringDialerSpec. +func (in *PeeringDialerSpec) DeepCopy() *PeeringDialerSpec { + if in == nil { + return nil + } + out := new(PeeringDialerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PeeringDialerStatus) DeepCopyInto(out *PeeringDialerStatus) { + *out = *in + if in.LastReconcileTime != nil { + in, out := &in.LastReconcileTime, &out.LastReconcileTime + *out = (*in).DeepCopy() + } + if in.ReconcileError != nil { + in, out := &in.ReconcileError, &out.ReconcileError + *out = new(ReconcileErrorStatus) + (*in).DeepCopyInto(*out) + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(SecretRefStatus) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeeringDialerStatus. +func (in *PeeringDialerStatus) DeepCopy() *PeeringDialerStatus { + if in == nil { + return nil + } + out := new(PeeringDialerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProxyDefaults) DeepCopyInto(out *ProxyDefaults) { *out = *in diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_peeringacceptors.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_peeringacceptors.yaml index 2b5d4e1e64..645a5e998d 100644 --- a/control-plane/config/crd/bases/consul.hashicorp.com_peeringacceptors.yaml +++ b/control-plane/config/crd/bases/consul.hashicorp.com_peeringacceptors.yaml @@ -78,8 +78,7 @@ spec: type: string type: object secret: - description: Secret shows any errors during the last reconciliation - of this resource. + description: SecretRef shows the status of the secret. properties: backend: description: 'Backend is where the generated secret is stored. @@ -89,7 +88,7 @@ spec: description: Key is the key of the secret generated. type: string latestHash: - description: LatestHash is the SHA256 sum of the secret generated. + description: ResourceVersion is the resource version for the secret. type: string name: description: Name is the name of the secret generated. diff --git a/control-plane/config/crd/bases/consul.hashicorp.com_peeringdialers.yaml b/control-plane/config/crd/bases/consul.hashicorp.com_peeringdialers.yaml new file mode 100644 index 0000000000..3a0c15b799 --- /dev/null +++ b/control-plane/config/crd/bases/consul.hashicorp.com_peeringdialers.yaml @@ -0,0 +1,108 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: peeringdialers.consul.hashicorp.com +spec: + group: consul.hashicorp.com + names: + kind: PeeringDialer + listKind: PeeringDialerList + plural: peeringdialers + singular: peeringdialer + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: PeeringDialer is the Schema for the peeringdialers 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: PeeringDialerSpec defines the desired state of PeeringDialer. + properties: + peer: + description: Peer describes the information needed to create a peering. + properties: + secret: + description: Secret describes how to store the generated peering + token. + properties: + backend: + description: 'Backend is where the generated secret is stored. + Currently supports the value: "kubernetes".' + type: string + key: + description: Key is the key of the secret generated. + type: string + name: + description: Name is the name of the secret generated. + type: string + type: object + type: object + required: + - peer + type: object + status: + description: PeeringDialerStatus defines the observed state of PeeringDialer. + properties: + lastReconcileTime: + description: LastReconcileTime is the last time the resource was reconciled. + format: date-time + type: string + reconcileError: + description: ReconcileError shows any errors during the last reconciliation + of this resource. + properties: + error: + description: Error is a boolean indicating if there was an error + during the last reconcile of this resource. + type: boolean + message: + description: Message displays the error message from the last + reconcile. + type: string + type: object + secret: + description: SecretRef shows the status of the secret. + properties: + backend: + description: 'Backend is where the generated secret is stored. + Currently supports the value: "kubernetes".' + type: string + key: + description: Key is the key of the secret generated. + type: string + latestHash: + description: ResourceVersion is the resource version for the secret. + type: string + name: + description: Name is the name of the secret generated. + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/control-plane/config/rbac/role.yaml b/control-plane/config/rbac/role.yaml index 2ede4842c1..8daf050ec0 100644 --- a/control-plane/config/rbac/role.yaml +++ b/control-plane/config/rbac/role.yaml @@ -103,6 +103,26 @@ rules: - get - patch - update +- apiGroups: + - consul.hashicorp.com + resources: + - peeringdialers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - consul.hashicorp.com + resources: + - peeringdialers/status + verbs: + - get + - patch + - update - apiGroups: - consul.hashicorp.com resources: diff --git a/control-plane/connect-inject/peering_dialer_controller.go b/control-plane/connect-inject/peering_dialer_controller.go new file mode 100644 index 0000000000..f9e1278682 --- /dev/null +++ b/control-plane/connect-inject/peering_dialer_controller.go @@ -0,0 +1,271 @@ +package connectinject + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/go-logr/logr" + "github.com/hashicorp/consul/api" + 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" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + consulv1alpha1 "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" +) + +// PeeringDialerController reconciles a PeeringDialer object. +type PeeringDialerController struct { + client.Client + // ConsulClient points at the agent local to the connect-inject deployment pod. + ConsulClient *api.Client + Log logr.Logger + Scheme *runtime.Scheme + context.Context +} + +//+kubebuilder:rbac:groups=consul.hashicorp.com,resources=peeringdialers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=consul.hashicorp.com,resources=peeringdialers/status,verbs=get;update;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *PeeringDialerController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log.Info("received request for PeeringDialer:", "name", req.Name, "ns", req.Namespace) + + // Get the PeeringDialer resource. + peeringDialer := &consulv1alpha1.PeeringDialer{} + err := r.Client.Get(ctx, req.NamespacedName, peeringDialer) + + // If the PeeringDialer resource has been deleted (and we get an IsNotFound + // error), we need to delete it in Consul. + if k8serrors.IsNotFound(err) { + r.Log.Info("PeeringDialer was deleted, deleting from Consul", "name", req.Name, "ns", req.Namespace) + _, err := r.deletePeering(ctx, req.Name) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } else if err != nil { + r.Log.Error(err, "failed to get PeeringDialer", "name", req.Name, "ns", req.Namespace) + return ctrl.Result{}, err + } + + // Get the status secret and the spec secret. + // Cases need to handle statusSecretSet, existingStatusSecret, specSecretSet, existingSpecSecret. + // no specSecretSet --> error bc spec needs to be set. + // no existingSpecSecret --> error bc waiting for spec secret to exist. + // no statusSecretSet, yes specSecretSet, no existingStatusSecret, yes existingSpecSecret --> initiate peering. + // yes statusSecretSet, yes specSecretSet, no existingStatusSecret, yes existingSpecSecret --> initiate peering. + // yes statusSecretSet, yes specSecretSet, yes existingStatusSecret, yes existingSpecSecret --> compare contents, if + // different initiate peering. + + // Get the status secret and the spec secret. + statusSecretSet := false + if peeringDialer.Status.SecretRef != nil { + statusSecretSet = true + } + // TODO(peering): remove this once CRD validation exists. + specSecretSet := false + if peeringDialer.Spec.Peer != nil { + if peeringDialer.Spec.Peer.Secret != nil { + specSecretSet = true + } + } + if !specSecretSet { + err = errors.New("PeeringDialer spec.peer.secret was not set") + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } + + // existingStatusSecret will be nil if the secret specified by the status doesn't exist. + var existingStatusSecret *corev1.Secret + if statusSecretSet { + _, existingStatusSecret, err = r.getExistingSecret(ctx, peeringDialer.Status.SecretRef.Name, peeringDialer.Namespace) + if err != nil { + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } + } + + // existingSpecSecret will be nil if the secret specified by the spec doesn't exist. + var existingSpecSecret *corev1.Secret + if specSecretSet { + _, existingSpecSecret, err = r.getExistingSecret(ctx, peeringDialer.Spec.Peer.Secret.Name, peeringDialer.Namespace) + if err != nil { + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } + } + + // If spec secret doesn't exist, error because we can only initiate peering if we have a token to initiate with. + if existingSpecSecret == nil { + err = errors.New("PeeringDialer spec.peer.secret does not exist") + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } + + // Read the peering from Consul. + // TODO(peering): do we need to pass in partition? + readReq := api.PeeringReadRequest{Name: peeringDialer.Name} + r.Log.Info("reading peering from Consul", "name", peeringDialer.Name) + peering, _, err := r.ConsulClient.Peerings().Read(ctx, readReq, nil) + var statusErr api.StatusError + peeringExists := true + if errors.As(err, &statusErr) && statusErr.Code == http.StatusNotFound && peering == nil { + peeringExists = false + } else if err != nil { + r.Log.Error(err, "failed to get Peering from Consul", "name", req.Name) + return ctrl.Result{}, err + } + // TODO(peering): Verify that the existing peering in Consul is an dialer peer. If it is an acceptor peer, an error should be thrown. + + // At this point, we know the spec secret exists. If the status secret doesn't + // exist, then we want to initiate peering and update the status with the secret for the token being used. + if existingStatusSecret == nil { + // Whether the peering exists in Consul or not we want to initiate the peering so the status can reflect the + // correct secret specified in the spec. + r.Log.Info("status.secret doesn't exist or wasn't set, establishing peering with the existing spec.peer.secret", "secret-name", peeringDialer.Spec.Peer.Secret.Name, "secret-namespace", peeringDialer.Namespace) + peeringToken := existingSpecSecret.Data[peeringDialer.Spec.Peer.Secret.Key] + _, err := r.initiatePeering(ctx, peeringDialer.Name, string(peeringToken)) + if err != nil { + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } else { + err := r.updateStatus(ctx, peeringDialer, existingSpecSecret.ResourceVersion) + return ctrl.Result{}, err + } + } else { + // At this point, the status secret does exist. + // If the peering in Consul does not exist, initiate peering. + if !peeringExists { + r.Log.Info("status.secret exists, but the peering doesn't exist in Consul; establishing peering with the existing spec.peer.secret", "secret-name", peeringDialer.Spec.Peer.Secret.Name, "secret-namespace", peeringDialer.Namespace) + peeringToken := existingSpecSecret.Data[peeringDialer.Spec.Peer.Secret.Key] + _, err := r.initiatePeering(ctx, peeringDialer.Name, string(peeringToken)) + if err != nil { + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } else { + err := r.updateStatus(ctx, peeringDialer, existingSpecSecret.ResourceVersion) + return ctrl.Result{}, err + } + } + + // Or, if the peering in Consul does exist, compare it to the contents of the spec's secret. If there's any + // differences, initiate peering. + if r.specStatusSecretsDifferent(peeringDialer, existingSpecSecret) { + r.Log.Info("status.secret exists and is different from spec.peer.secret; establishing peering with the existing spec.peer.secret", "secret-name", peeringDialer.Spec.Peer.Secret.Name, "secret-namespace", peeringDialer.Namespace) + peeringToken := existingSpecSecret.Data[peeringDialer.Spec.Peer.Secret.Key] + _, err := r.initiatePeering(ctx, peeringDialer.Name, string(peeringToken)) + if err != nil { + _ = r.updateStatusError(ctx, peeringDialer, err) + return ctrl.Result{}, err + } else { + err := r.updateStatus(ctx, peeringDialer, existingSpecSecret.ResourceVersion) + return ctrl.Result{}, err + } + } + } + + return ctrl.Result{}, nil +} + +func (r *PeeringDialerController) specStatusSecretsDifferent(peeringDialer *consulv1alpha1.PeeringDialer, existingSpecSecret *corev1.Secret) bool { + if peeringDialer.Status.SecretRef.Name != peeringDialer.Spec.Peer.Secret.Name { + return true + } + if peeringDialer.Status.SecretRef.Key != peeringDialer.Spec.Peer.Secret.Key { + return true + } + if peeringDialer.Status.SecretRef.Backend != peeringDialer.Spec.Peer.Secret.Backend { + return true + } + existingSpecSecretResourceVersion := existingSpecSecret.ResourceVersion + return existingSpecSecretResourceVersion != peeringDialer.Status.SecretRef.ResourceVersion +} + +func (r *PeeringDialerController) updateStatus(ctx context.Context, peeringDialer *consulv1alpha1.PeeringDialer, resourceVersion string) error { + peeringDialer.Status.SecretRef = &consulv1alpha1.SecretRefStatus{ + Name: peeringDialer.Spec.Peer.Secret.Name, + Key: peeringDialer.Spec.Peer.Secret.Key, + Backend: peeringDialer.Spec.Peer.Secret.Backend, + } + + peeringDialer.Status.SecretRef.ResourceVersion = resourceVersion + + peeringDialer.Status.LastReconcileTime = &metav1.Time{Time: time.Now()} + err := r.Status().Update(ctx, peeringDialer) + if err != nil { + r.Log.Error(err, "failed to update PeeringDialer status", "name", peeringDialer.Name, "namespace", peeringDialer.Namespace) + } + return err +} + +func (r *PeeringDialerController) updateStatusError(ctx context.Context, peeringDialer *consulv1alpha1.PeeringDialer, reconcileErr error) error { + peeringDialer.Status.ReconcileError = &consulv1alpha1.ReconcileErrorStatus{ + Error: pointerToBool(true), + Message: pointerToString(reconcileErr.Error()), + } + + peeringDialer.Status.LastReconcileTime = &metav1.Time{Time: time.Now()} + err := r.Status().Update(ctx, peeringDialer) + if err != nil { + r.Log.Error(err, "failed to update PeeringDialer status", "name", peeringDialer.Name, "namespace", peeringDialer.Namespace) + } + return err +} + +func (r *PeeringDialerController) getExistingSecret(ctx context.Context, name string, namespace string) (bool, *corev1.Secret, error) { + existingSecret := &corev1.Secret{} + namespacedName := types.NamespacedName{ + Name: name, + Namespace: namespace, + } + err := r.Client.Get(ctx, namespacedName, existingSecret) + if k8serrors.IsNotFound(err) { + // The secret was deleted. + return false, nil, nil + } else if err != nil { + r.Log.Error(err, "couldn't get secret", "name", name, "namespace", namespace) + return false, nil, err + } + return true, existingSecret, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PeeringDialerController) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&consulv1alpha1.PeeringDialer{}). + Complete(r) +} + +// initiatePeering is a helper function that calls the Consul api to generate a token for the peer. +func (r *PeeringDialerController) initiatePeering(ctx context.Context, peerName string, peeringToken string) (*api.PeeringInitiateResponse, error) { + req := api.PeeringInitiateRequest{ + PeerName: peerName, + PeeringToken: peeringToken, + } + resp, _, err := r.ConsulClient.Peerings().Initiate(ctx, req, nil) + if err != nil { + r.Log.Error(err, "failed to initiate peering", "err", err) + return nil, err + } + return resp, nil +} + +// deletePeering is a helper function that calls the Consul api to delete a peering. +func (r *PeeringDialerController) deletePeering(ctx context.Context, peerName string) (*api.PeeringDeleteResponse, error) { + deleteReq := api.PeeringDeleteRequest{ + Name: peerName, + } + resp, _, err := r.ConsulClient.Peerings().Delete(ctx, deleteReq, nil) + if err != nil { + r.Log.Error(err, "failed to delete Peering from Consul", "name", peerName) + return nil, err + } + return resp, nil +} diff --git a/control-plane/connect-inject/peering_dialer_controller_test.go b/control-plane/connect-inject/peering_dialer_controller_test.go new file mode 100644 index 0000000000..f288a68939 --- /dev/null +++ b/control-plane/connect-inject/peering_dialer_controller_test.go @@ -0,0 +1,102 @@ +package connectinject + +import ( + "context" + "net/http" + "testing" + + logrtest "github.com/go-logr/logr/testing" + "github.com/hashicorp/consul-k8s/control-plane/api/v1alpha1" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/sdk/testutil" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// TestReconcileDeletePeeringDialer reconciles a PeeringDialer resource that is no longer in Kubernetes, but still +// exists in Consul. +func TestReconcileDeletePeeringDialer(t *testing.T) { + t.Parallel() + nodeName := "test-node" + cases := []struct { + name string + initialConsulPeerNames []string + expErr string + }{ + { + name: "PeeringDialer no longer in K8s, still exists in Consul", + initialConsulPeerNames: []string{ + "dialer-deleted", + }, + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + // Add the default namespace. + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}} + + // Create fake k8s client. + k8sObjects := []runtime.Object{&ns} + + // Add peering types to the scheme. + s := scheme.Scheme + s.AddKnownTypes(v1alpha1.GroupVersion, &v1alpha1.PeeringDialer{}, &v1alpha1.PeeringDialerList{}) + fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(k8sObjects...).Build() + + // Create test consul server. + consul, err := testutil.NewTestServerConfigT(t, func(c *testutil.TestServerConfig) { + c.NodeName = nodeName + }) + require.NoError(t, err) + defer consul.Stop() + consul.WaitForServiceIntentions(t) + + cfg := &api.Config{ + Address: consul.HTTPAddr, + } + consulClient, err := api.NewClient(cfg) + require.NoError(t, err) + + // Add the initial peerings into Consul by calling the Generate token endpoint. + _, _, err = consulClient.Peerings().GenerateToken(context.Background(), api.PeeringGenerateTokenRequest{PeerName: tt.initialConsulPeerNames[0]}, nil) + require.NoError(t, err) + + // Create the peering acceptor controller. + pdc := &PeeringDialerController{ + Client: fakeClient, + Log: logrtest.TestLogger{T: t}, + ConsulClient: consulClient, + Scheme: s, + } + namespacedName := types.NamespacedName{ + Name: "dialer-deleted", + Namespace: "default", + } + + // Reconcile a resource that is not in K8s, but is still in Consul. + resp, err := pdc.Reconcile(context.Background(), ctrl.Request{ + NamespacedName: namespacedName, + }) + if tt.expErr != "" { + require.EqualError(t, err, tt.expErr) + } else { + require.NoError(t, err) + } + require.False(t, resp.Requeue) + + // After reconciliation, Consul should not have the peering. + readReq := api.PeeringReadRequest{Name: "dialer-deleted"} + peering, _, err := consulClient.Peerings().Read(context.Background(), readReq, nil) + var statusErr api.StatusError + require.ErrorAs(t, err, &statusErr) + require.Equal(t, http.StatusNotFound, statusErr.Code) + require.Nil(t, peering) + }) + } +} diff --git a/control-plane/subcommand/inject-connect/command.go b/control-plane/subcommand/inject-connect/command.go index dcdf71decd..cfd11e4e95 100644 --- a/control-plane/subcommand/inject-connect/command.go +++ b/control-plane/subcommand/inject-connect/command.go @@ -435,11 +435,21 @@ func (c *Command) Run(args []string) int { if err = (&connectinject.PeeringAcceptorController{ Client: mgr.GetClient(), ConsulClient: c.consulClient, - Log: ctrl.Log.WithName("controller").WithName("peering"), + Log: ctrl.Log.WithName("controller").WithName("peering-acceptor"), Scheme: mgr.GetScheme(), Context: ctx, }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "peering") + setupLog.Error(err, "unable to create controller", "controller", "peering-acceptor") + return 1 + } + if err = (&connectinject.PeeringDialerController{ + Client: mgr.GetClient(), + ConsulClient: c.consulClient, + Log: ctrl.Log.WithName("controller").WithName("peering-dialer"), + Scheme: mgr.GetScheme(), + Context: ctx, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "peering-dialer") return 1 } mgr.GetWebhookServer().CertDir = c.flagCertDir