From fb9bcf509a9df702ccd3e97477beba76ed8c3af0 Mon Sep 17 00:00:00 2001 From: Andrew Harding Date: Tue, 27 Jun 2023 07:54:44 -0600 Subject: [PATCH] Added ClusterStaticEntry support Signed-off-by: Andrew Harding --- .kubebuilder-hist | 1 + PROJECT | 8 + README.md | 37 ++- api/v1alpha1/clusterstaticentry_types.go | 76 +++++ api/v1alpha1/zz_generated.deepcopy.go | 106 +++++++ .../spire.spiffe.io_clusterstaticentries.yaml | 96 +++++++ config/crd/kustomization.yaml | 3 + .../cainjection_in_clusterstaticentries.yaml | 7 + .../webhook_in_clusterstaticentries.yaml | 16 ++ .../rbac/clusterstaticentry_editor_role.yaml | 24 ++ .../rbac/clusterstaticentry_viewer_role.yaml | 20 ++ config/rbac/role.yaml | 26 ++ .../spire_v1alpha1_clusterstaticentry.yaml | 6 + controllers/clusterstaticentry_controller.go | 53 ++++ demo/README.md | 16 +- demo/config/cluster1/crd-rbac/role.yaml | 9 + .../spire.spiffe.io_clusterstaticentries.yaml | 96 +++++++ demo/config/cluster1/kustomization.yaml | 1 + demo/config/cluster1/spire/spire-agent.yaml | 4 +- demo/config/cluster1/spire/spire-server.yaml | 2 +- demo/config/cluster2/crd-rbac/role.yaml | 9 + .../spire.spiffe.io_clusterstaticentries.yaml | 96 +++++++ demo/config/cluster2/kustomization.yaml | 1 + demo/config/cluster2/spire/spire-agent.yaml | 4 +- demo/config/cluster2/spire/spire-server.yaml | 2 +- demo/config/static-entry.yaml | 15 + demo/scripts/show-spire-entries.sh | 3 +- demo/test.sh | 34 ++- docs/clusterstaticentry-crd.md | 30 ++ main.go | 8 + pkg/k8sapi/helpers.go | 8 + pkg/spireapi/types.go | 6 + pkg/spireentry/by.go | 60 ++++ pkg/spireentry/entries.go | 47 +++ pkg/spireentry/logging.go | 29 +- pkg/spireentry/reconciler.go | 268 +++++++++++------- 36 files changed, 1091 insertions(+), 136 deletions(-) create mode 100644 api/v1alpha1/clusterstaticentry_types.go create mode 100644 config/crd/bases/spire.spiffe.io_clusterstaticentries.yaml create mode 100644 config/crd/patches/cainjection_in_clusterstaticentries.yaml create mode 100644 config/crd/patches/webhook_in_clusterstaticentries.yaml create mode 100644 config/rbac/clusterstaticentry_editor_role.yaml create mode 100644 config/rbac/clusterstaticentry_viewer_role.yaml create mode 100644 config/samples/spire_v1alpha1_clusterstaticentry.yaml create mode 100644 controllers/clusterstaticentry_controller.go create mode 100644 demo/config/cluster1/crd/spire.spiffe.io_clusterstaticentries.yaml create mode 100644 demo/config/cluster2/crd/spire.spiffe.io_clusterstaticentries.yaml create mode 100644 demo/config/static-entry.yaml create mode 100644 docs/clusterstaticentry-crd.md create mode 100644 pkg/spireentry/by.go diff --git a/.kubebuilder-hist b/.kubebuilder-hist index c04da7cf..a7fbb3b0 100644 --- a/.kubebuilder-hist +++ b/.kubebuilder-hist @@ -4,3 +4,4 @@ v3.2.0: .scripts/kubebuilder create api --resource --controller --group spire -- v3.2.0: .scripts/kubebuilder create api --group spire --version v1alpha1 --kind ControllerManagerConfig --resource --controller=false --make=false v3.2.0: .scripts/kubebuilder create webhook --programmatic-validation --kind ClusterFederatedTrustDomain --version v1alpha1 --group spire v3.2.0: .scripts/kubebuilder create webhook --programmatic-validation --kind ClusterSPIFFEID --version v1alpha1 --group spire +v3.3.0: .scripts/kubebuilder create api --resource --controller --group spire --version v1alpha1 --namespaced=false --kind ClusterStaticEntry diff --git a/PROJECT b/PROJECT index 88120bab..c4bd82ce 100644 --- a/PROJECT +++ b/PROJECT @@ -34,4 +34,12 @@ resources: kind: ControllerManagerConfig path: github.com/spiffe/spire-controller-manager/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + controller: true + domain: spiffe.io + group: spire + kind: ClusterStaticEntry + path: github.com/spiffe/spire-controller-manager/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index bc46a519..9d4322ac 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,14 @@ The [ClusterFederatedTrustDomain](docs/clusterfederatedtrustdomain-crd.md) resource is a cluster scoped CRD that describes a federation relationship for the cluster. +### ClusterStaticEntry + +The [ClusterStaticEntry](docs/clusterstaticentry-crd.md) resource is a cluster +scoped CRD that describes a static SPIRE registration entry. It is typically +used for registering workloads that do not run in the Kubernetes cluster but +otherwise need to be part of the trust domain (e.g. downstream nested SPIRE +servers). + ### Reconciliation #### Workload Registration @@ -33,12 +41,14 @@ controllers against the following resources: - [Pods](https://kubernetes.io/docs/concepts/workloads/pods/) - [ClusterSPIFFEID](docs/clusterspiffeid-crd.md) +- [ClusterStaticEntry](docs/clusterstaticentry-crd.md) When changes are detected on these resources, a workload reconciliation process is triggered. This process determines which SPIRE entries should exist based on -the existing Pods and ClusterSPIFFEID resources which apply to those pods. It -creates, updates, and deletes entries on SPIRE server as appropriate to match -the declared state. +the existing Pods and ClusterSPIFFEID resources which apply to those pods, as +well as static entries declared via ClusterStaticEntry resources. The +reconciliation process creates, updates, and deletes entries on SPIRE server as +appropriate to match the declared state. #### Federation @@ -64,6 +74,27 @@ The [demo](demo) includes [sample configuration](demo/config/cluster1) for deploying the SPIRE Controller Manager, SPIRE, and the SPIFFE CSI driver, including requisite RBAC and Webhook configuration. +## Compatibility + +The SPIRE APIs used by the SPIRE Controller Manager are generally stable and +supported since at least SPIRE v1.0. However, the API has gained support for +additional entry fields beyond what was supported in SPIRE v1.0. Notably, these +include both the `jwt_svid_ttl` and the `hint` fields. The ClusterStaticEntry +CRD allows these fields to be set, however, a SPIRE server that does not +support these fields will not retain them. This means if these fields are set +on a ClusterStaticEntry with an older version of SPIRE, the SPIRE Controller +Manager will continously try to reconcile SPIRE server. In order to use these +fields, you must be on a version of SPIRE Server which supports them. + +At the moment, SPIRE Controller Manager will silently try and reconcile these +fields over and over. Future updates may cause the SPIRE Controller Manager +to fail when an unsupporting SPIRE Server is encounted while these fields +are set. + +The `hint` field is supported as of SPIRE 1.6.3. + +The `jwt_svid_ttl` field is supported as of SPIRE 1.5.0. + ## Demo [Link](demo) diff --git a/api/v1alpha1/clusterstaticentry_types.go b/api/v1alpha1/clusterstaticentry_types.go new file mode 100644 index 00000000..792ce354 --- /dev/null +++ b/api/v1alpha1/clusterstaticentry_types.go @@ -0,0 +1,76 @@ +/* +Copyright 2021 SPIRE Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ClusterStaticEntrySpec defines the desired state of ClusterStaticEntry +type ClusterStaticEntrySpec struct { + SPIFFEID string `json:"spiffeID"` + ParentID string `json:"parentID"` + Selectors []string `json:"selectors"` + FederatesWith []string `json:"federatesWith"` + X509SVIDTTL metav1.Duration `json:"x509SVIDTTL"` + JWTSVIDTTL metav1.Duration `json:"jwtSVIDTTL"` + DNSNames []string `json:"dnsNames"` + Hint string `json:"hint"` + Admin bool `json:"admin,omitempty"` + Downstream bool `json:"downstream,omitempty"` +} + +// ClusterStaticEntryStatus defines the observed state of ClusterStaticEntry +type ClusterStaticEntryStatus struct { + // If the static entry rendered properly. + Rendered bool `json:"rendered"` + + // If the static entry was masked by another entry. + Masked bool `json:"masked"` + + // If the static entry was successfully created/updated. + Set bool `json:"set"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster + +// ClusterStaticEntry is the Schema for the clusterstaticentries API +type ClusterStaticEntry struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterStaticEntrySpec `json:"spec,omitempty"` + Status ClusterStaticEntryStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ClusterStaticEntryList contains a list of ClusterStaticEntry +type ClusterStaticEntryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterStaticEntry `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterStaticEntry{}, &ClusterStaticEntryList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index cbf3a6e9..3c3e8fcb 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -264,6 +264,112 @@ func (in *ClusterSPIFFEIDStatus) DeepCopy() *ClusterSPIFFEIDStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStaticEntry) DeepCopyInto(out *ClusterStaticEntry) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStaticEntry. +func (in *ClusterStaticEntry) DeepCopy() *ClusterStaticEntry { + if in == nil { + return nil + } + out := new(ClusterStaticEntry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterStaticEntry) 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 *ClusterStaticEntryList) DeepCopyInto(out *ClusterStaticEntryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterStaticEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStaticEntryList. +func (in *ClusterStaticEntryList) DeepCopy() *ClusterStaticEntryList { + if in == nil { + return nil + } + out := new(ClusterStaticEntryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterStaticEntryList) 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 *ClusterStaticEntrySpec) DeepCopyInto(out *ClusterStaticEntrySpec) { + *out = *in + if in.Selectors != nil { + in, out := &in.Selectors, &out.Selectors + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.FederatesWith != nil { + in, out := &in.FederatesWith, &out.FederatesWith + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.X509SVIDTTL = in.X509SVIDTTL + out.JWTSVIDTTL = in.JWTSVIDTTL + if in.DNSNames != nil { + in, out := &in.DNSNames, &out.DNSNames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStaticEntrySpec. +func (in *ClusterStaticEntrySpec) DeepCopy() *ClusterStaticEntrySpec { + if in == nil { + return nil + } + out := new(ClusterStaticEntrySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterStaticEntryStatus) DeepCopyInto(out *ClusterStaticEntryStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStaticEntryStatus. +func (in *ClusterStaticEntryStatus) DeepCopy() *ClusterStaticEntryStatus { + if in == nil { + return nil + } + out := new(ClusterStaticEntryStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ControllerConfigurationSpec) DeepCopyInto(out *ControllerConfigurationSpec) { *out = *in diff --git a/config/crd/bases/spire.spiffe.io_clusterstaticentries.yaml b/config/crd/bases/spire.spiffe.io_clusterstaticentries.yaml new file mode 100644 index 00000000..23b80997 --- /dev/null +++ b/config/crd/bases/spire.spiffe.io_clusterstaticentries.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: clusterstaticentries.spire.spiffe.io +spec: + group: spire.spiffe.io + names: + kind: ClusterStaticEntry + listKind: ClusterStaticEntryList + plural: clusterstaticentries + singular: clusterstaticentry + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterStaticEntry is the Schema for the clusterstaticentries + 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: ClusterStaticEntrySpec defines the desired state of ClusterStaticEntry + properties: + admin: + type: boolean + dnsNames: + items: + type: string + type: array + downstream: + type: boolean + federatesWith: + items: + type: string + type: array + hint: + type: string + jwtSVIDTTL: + type: string + parentID: + type: string + selectors: + items: + type: string + type: array + spiffeID: + type: string + x509SVIDTTL: + type: string + required: + - dnsNames + - federatesWith + - hint + - jwtSVIDTTL + - parentID + - selectors + - spiffeID + - x509SVIDTTL + type: object + status: + description: ClusterStaticEntryStatus defines the observed state of ClusterStaticEntry + properties: + masked: + description: If the static entry was masked by another entry. + type: boolean + rendered: + description: If the static entry rendered properly. + type: boolean + set: + description: If the static entry was successfully created/updated. + type: boolean + required: + - masked + - rendered + - set + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 92756d26..c31ebc0c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/spire.spiffe.io_clusterspiffeids.yaml - bases/spire.spiffe.io_clusterfederatedtrustdomains.yaml - bases/spire.spiffe.io_controllermanagerconfigs.yaml +- bases/spire.spiffe.io_clusterstaticentries.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -13,6 +14,7 @@ patchesStrategicMerge: #- patches/webhook_in_clusterspiffeids.yaml #- patches/webhook_in_clusterfederatedtrustdomains.yaml #- patches/webhook_in_controllermanagerconfigs.yaml +#- patches/webhook_in_clusterstaticentries.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -20,6 +22,7 @@ patchesStrategicMerge: #- patches/cainjection_in_clusterspiffeids.yaml #- patches/cainjection_in_clusterfederatedtrustdomains.yaml #- patches/cainjection_in_controllermanagerconfigs.yaml +#- patches/cainjection_in_clusterstaticentries.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_clusterstaticentries.yaml b/config/crd/patches/cainjection_in_clusterstaticentries.yaml new file mode 100644 index 00000000..e7442509 --- /dev/null +++ b/config/crd/patches/cainjection_in_clusterstaticentries.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: clusterstaticentries.spire.spiffe.io diff --git a/config/crd/patches/webhook_in_clusterstaticentries.yaml b/config/crd/patches/webhook_in_clusterstaticentries.yaml new file mode 100644 index 00000000..9d031693 --- /dev/null +++ b/config/crd/patches/webhook_in_clusterstaticentries.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterstaticentries.spire.spiffe.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/rbac/clusterstaticentry_editor_role.yaml b/config/rbac/clusterstaticentry_editor_role.yaml new file mode 100644 index 00000000..449c38db --- /dev/null +++ b/config/rbac/clusterstaticentry_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit clusterstaticentries. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: clusterstaticentry-editor-role +rules: +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries/status + verbs: + - get diff --git a/config/rbac/clusterstaticentry_viewer_role.yaml b/config/rbac/clusterstaticentry_viewer_role.yaml new file mode 100644 index 00000000..29624dd6 --- /dev/null +++ b/config/rbac/clusterstaticentry_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view clusterstaticentries. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: clusterstaticentry-viewer-role +rules: +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries + verbs: + - get + - list + - watch +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 30e34a3b..b7ba1366 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -81,3 +81,29 @@ rules: - get - patch - update +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries/finalizers + verbs: + - update +- apiGroups: + - spire.spiffe.io + resources: + - clusterstaticentries/status + verbs: + - get + - patch + - update diff --git a/config/samples/spire_v1alpha1_clusterstaticentry.yaml b/config/samples/spire_v1alpha1_clusterstaticentry.yaml new file mode 100644 index 00000000..a092682a --- /dev/null +++ b/config/samples/spire_v1alpha1_clusterstaticentry.yaml @@ -0,0 +1,6 @@ +apiVersion: spire.spiffe.io/v1alpha1 +kind: ClusterStaticEntry +metadata: + name: clusterstaticentry-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/clusterstaticentry_controller.go b/controllers/clusterstaticentry_controller.go new file mode 100644 index 00000000..88ed0f62 --- /dev/null +++ b/controllers/clusterstaticentry_controller.go @@ -0,0 +1,53 @@ +/* +Copyright 2021 SPIRE Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + spirev1alpha1 "github.com/spiffe/spire-controller-manager/api/v1alpha1" + "github.com/spiffe/spire-controller-manager/pkg/reconciler" +) + +// ClusterStaticEntryReconciler reconciles a ClusterStaticEntry object +type ClusterStaticEntryReconciler struct { + client.Client + Scheme *runtime.Scheme + Triggerer reconciler.Triggerer +} + +//+kubebuilder:rbac:groups=spire.spiffe.io,resources=clusterstaticentries,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=spire.spiffe.io,resources=clusterstaticentries/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=spire.spiffe.io,resources=clusterstaticentries/finalizers,verbs=update + +func (r *ClusterStaticEntryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log.FromContext(ctx).V(1).Info("Triggering reconciliation") + r.Triggerer.Trigger() + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ClusterStaticEntryReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&spirev1alpha1.ClusterStaticEntry{}). + Complete(r) +} diff --git a/demo/README.md b/demo/README.md index 4d7d14a1..bb592697 100644 --- a/demo/README.md +++ b/demo/README.md @@ -33,9 +33,9 @@ Build the greeter server and client: Pull the requisite images: - $ echo ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ - ghcr.io/spiffe/spiffe-csi-driver:0.2.0 \ + $ echo ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ + ghcr.io/spiffe/spiffe-csi-driver:0.2.3 \ ghcr.io/spiffe/spire-controller-manager:nightly \ | xargs -n1 docker pull @@ -43,9 +43,9 @@ Start up cluster1 and load the requisite images: $ ./cluster1 kind create cluster $ echo \ - ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ - ghcr.io/spiffe/spiffe-csi-driver:0.2.0 \ + ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ + ghcr.io/spiffe/spiffe-csi-driver:0.2.3 \ ghcr.io/spiffe/spire-controller-manager:nightly \ greeter-server:demo \ | xargs -n1 ./cluster1 kind load docker-image @@ -54,8 +54,8 @@ Start up cluster 2 and load the requisite images: $ ./cluster2 kind create cluster $ echo \ - ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ + ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ ghcr.io/spiffe/spiffe-csi-driver:nightly \ ghcr.io/spiffe/spire-controller-manager:nightly \ greeter-client:demo \ diff --git a/demo/config/cluster1/crd-rbac/role.yaml b/demo/config/cluster1/crd-rbac/role.yaml index 77c92327..7b0a379c 100644 --- a/demo/config/cluster1/crd-rbac/role.yaml +++ b/demo/config/cluster1/crd-rbac/role.yaml @@ -33,3 +33,12 @@ rules: - apiGroups: ["spire.spiffe.io"] resources: ["clusterspiffeids/status"] verbs: ["get", "patch", "update"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries/finalizers"] + verbs: ["update"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries/status"] + verbs: ["get", "patch", "update"] diff --git a/demo/config/cluster1/crd/spire.spiffe.io_clusterstaticentries.yaml b/demo/config/cluster1/crd/spire.spiffe.io_clusterstaticentries.yaml new file mode 100644 index 00000000..23b80997 --- /dev/null +++ b/demo/config/cluster1/crd/spire.spiffe.io_clusterstaticentries.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: clusterstaticentries.spire.spiffe.io +spec: + group: spire.spiffe.io + names: + kind: ClusterStaticEntry + listKind: ClusterStaticEntryList + plural: clusterstaticentries + singular: clusterstaticentry + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterStaticEntry is the Schema for the clusterstaticentries + 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: ClusterStaticEntrySpec defines the desired state of ClusterStaticEntry + properties: + admin: + type: boolean + dnsNames: + items: + type: string + type: array + downstream: + type: boolean + federatesWith: + items: + type: string + type: array + hint: + type: string + jwtSVIDTTL: + type: string + parentID: + type: string + selectors: + items: + type: string + type: array + spiffeID: + type: string + x509SVIDTTL: + type: string + required: + - dnsNames + - federatesWith + - hint + - jwtSVIDTTL + - parentID + - selectors + - spiffeID + - x509SVIDTTL + type: object + status: + description: ClusterStaticEntryStatus defines the observed state of ClusterStaticEntry + properties: + masked: + description: If the static entry was masked by another entry. + type: boolean + rendered: + description: If the static entry rendered properly. + type: boolean + set: + description: If the static entry was successfully created/updated. + type: boolean + required: + - masked + - rendered + - set + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/demo/config/cluster1/kustomization.yaml b/demo/config/cluster1/kustomization.yaml index fe83e799..217a1ccb 100644 --- a/demo/config/cluster1/kustomization.yaml +++ b/demo/config/cluster1/kustomization.yaml @@ -3,6 +3,7 @@ resources: - spire/spire-namespace.yaml - crd/spire.spiffe.io_clusterfederatedtrustdomains.yaml - crd/spire.spiffe.io_clusterspiffeids.yaml +- crd/spire.spiffe.io_clusterstaticentries.yaml - crd-rbac/role.yaml - crd-rbac/role_binding.yaml - crd-rbac/leader_election_role.yaml diff --git a/demo/config/cluster1/spire/spire-agent.yaml b/demo/config/cluster1/spire/spire-agent.yaml index dade00fd..02474a11 100644 --- a/demo/config/cluster1/spire/spire-agent.yaml +++ b/demo/config/cluster1/spire/spire-agent.yaml @@ -103,7 +103,7 @@ spec: serviceAccountName: spire-agent containers: - name: spire-agent - image: ghcr.io/spiffe/spire-agent:1.2.3 + image: ghcr.io/spiffe/spire-agent:1.7.0 imagePullPolicy: IfNotPresent args: ["-config", "/run/spire/config/agent.conf"] env: @@ -124,7 +124,7 @@ spec: mountPath: /run/spire/sockets # This is the container which runs the SPIFFE CSI driver. - name: spiffe-csi-driver - image: ghcr.io/spiffe/spiffe-csi-driver:0.2.0 + image: ghcr.io/spiffe/spiffe-csi-driver:0.2.3 imagePullPolicy: IfNotPresent args: [ "-workload-api-socket-dir", "/spire-agent-socket", diff --git a/demo/config/cluster1/spire/spire-server.yaml b/demo/config/cluster1/spire/spire-server.yaml index 7be18859..490a7870 100644 --- a/demo/config/cluster1/spire/spire-server.yaml +++ b/demo/config/cluster1/spire/spire-server.yaml @@ -176,7 +176,7 @@ spec: shareProcessNamespace: true containers: - name: spire-server - image: ghcr.io/spiffe/spire-server:1.2.3 + image: ghcr.io/spiffe/spire-server:1.7.0 imagePullPolicy: IfNotPresent args: ["-config", "/run/spire/server/config/server.conf"] ports: diff --git a/demo/config/cluster2/crd-rbac/role.yaml b/demo/config/cluster2/crd-rbac/role.yaml index 77c92327..7b0a379c 100644 --- a/demo/config/cluster2/crd-rbac/role.yaml +++ b/demo/config/cluster2/crd-rbac/role.yaml @@ -33,3 +33,12 @@ rules: - apiGroups: ["spire.spiffe.io"] resources: ["clusterspiffeids/status"] verbs: ["get", "patch", "update"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries/finalizers"] + verbs: ["update"] + - apiGroups: ["spire.spiffe.io"] + resources: ["clusterstaticentries/status"] + verbs: ["get", "patch", "update"] diff --git a/demo/config/cluster2/crd/spire.spiffe.io_clusterstaticentries.yaml b/demo/config/cluster2/crd/spire.spiffe.io_clusterstaticentries.yaml new file mode 100644 index 00000000..23b80997 --- /dev/null +++ b/demo/config/cluster2/crd/spire.spiffe.io_clusterstaticentries.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: clusterstaticentries.spire.spiffe.io +spec: + group: spire.spiffe.io + names: + kind: ClusterStaticEntry + listKind: ClusterStaticEntryList + plural: clusterstaticentries + singular: clusterstaticentry + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterStaticEntry is the Schema for the clusterstaticentries + 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: ClusterStaticEntrySpec defines the desired state of ClusterStaticEntry + properties: + admin: + type: boolean + dnsNames: + items: + type: string + type: array + downstream: + type: boolean + federatesWith: + items: + type: string + type: array + hint: + type: string + jwtSVIDTTL: + type: string + parentID: + type: string + selectors: + items: + type: string + type: array + spiffeID: + type: string + x509SVIDTTL: + type: string + required: + - dnsNames + - federatesWith + - hint + - jwtSVIDTTL + - parentID + - selectors + - spiffeID + - x509SVIDTTL + type: object + status: + description: ClusterStaticEntryStatus defines the observed state of ClusterStaticEntry + properties: + masked: + description: If the static entry was masked by another entry. + type: boolean + rendered: + description: If the static entry rendered properly. + type: boolean + set: + description: If the static entry was successfully created/updated. + type: boolean + required: + - masked + - rendered + - set + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/demo/config/cluster2/kustomization.yaml b/demo/config/cluster2/kustomization.yaml index fe83e799..217a1ccb 100644 --- a/demo/config/cluster2/kustomization.yaml +++ b/demo/config/cluster2/kustomization.yaml @@ -3,6 +3,7 @@ resources: - spire/spire-namespace.yaml - crd/spire.spiffe.io_clusterfederatedtrustdomains.yaml - crd/spire.spiffe.io_clusterspiffeids.yaml +- crd/spire.spiffe.io_clusterstaticentries.yaml - crd-rbac/role.yaml - crd-rbac/role_binding.yaml - crd-rbac/leader_election_role.yaml diff --git a/demo/config/cluster2/spire/spire-agent.yaml b/demo/config/cluster2/spire/spire-agent.yaml index f303e6b6..c8953a08 100644 --- a/demo/config/cluster2/spire/spire-agent.yaml +++ b/demo/config/cluster2/spire/spire-agent.yaml @@ -103,7 +103,7 @@ spec: serviceAccountName: spire-agent containers: - name: spire-agent - image: ghcr.io/spiffe/spire-agent:1.2.3 + image: ghcr.io/spiffe/spire-agent:1.7.0 imagePullPolicy: IfNotPresent args: ["-config", "/run/spire/config/agent.conf"] env: @@ -124,7 +124,7 @@ spec: mountPath: /run/spire/sockets # This is the container which runs the SPIFFE CSI driver. - name: spiffe-csi-driver - image: ghcr.io/spiffe/spiffe-csi-driver:0.2.0 + image: ghcr.io/spiffe/spiffe-csi-driver:0.2.3 imagePullPolicy: IfNotPresent args: [ "-workload-api-socket-dir", "/spire-agent-socket", diff --git a/demo/config/cluster2/spire/spire-server.yaml b/demo/config/cluster2/spire/spire-server.yaml index 771818a3..43561dbb 100644 --- a/demo/config/cluster2/spire/spire-server.yaml +++ b/demo/config/cluster2/spire/spire-server.yaml @@ -176,7 +176,7 @@ spec: shareProcessNamespace: true containers: - name: spire-server - image: ghcr.io/spiffe/spire-server:1.2.3 + image: ghcr.io/spiffe/spire-server:1.7.0 imagePullPolicy: IfNotPresent args: ["-config", "/run/spire/server/config/server.conf"] ports: diff --git a/demo/config/static-entry.yaml b/demo/config/static-entry.yaml new file mode 100644 index 00000000..8d9db42b --- /dev/null +++ b/demo/config/static-entry.yaml @@ -0,0 +1,15 @@ +apiVersion: spire.spiffe.io/v1alpha1 +kind: ClusterStaticEntry +metadata: + name: static-entry +spec: + spiffeID: spiffe://cluster1.demo/static-spiffe-id + parentID: spiffe://cluster1.demo/static-parent-id + selectors: ["static:one", "static:two"] + federatesWith: ["cluster1.demo"] + x509SVIDTTL: "2h" + jwtSVIDTTL: "6m" + dnsNames: ["static-dns"] + hint: "static-hint-2" + admin: true + downstream: true diff --git a/demo/scripts/show-spire-entries.sh b/demo/scripts/show-spire-entries.sh index 63e01ebd..9a401ebb 100755 --- a/demo/scripts/show-spire-entries.sh +++ b/demo/scripts/show-spire-entries.sh @@ -5,4 +5,5 @@ set -eo pipefail kubectl exec -t \ -nspire-system \ -c spire-server deployment/spire-server -- \ - /opt/spire/bin/spire-server entry show + /opt/spire/bin/spire-server entry show \ + "$@" diff --git a/demo/test.sh b/demo/test.sh index ed7645b9..2208fe2b 100755 --- a/demo/test.sh +++ b/demo/test.sh @@ -46,8 +46,8 @@ log-info "Building greeter server/client..." (cd greeter; make docker-build) log-info "Pulling docker images..." -echo ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ +echo ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ ghcr.io/spiffe/spiffe-csi-driver:0.2.0 \ | xargs -n1 docker pull @@ -59,8 +59,8 @@ log-info "Creating cluster2..." log-info "Loading images into cluster1..." echo \ - ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ + ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ ghcr.io/spiffe/spiffe-csi-driver:0.2.0 \ ghcr.io/spiffe/spire-controller-manager:nightly \ greeter-server:demo \ @@ -68,8 +68,8 @@ echo \ log-info "Loading images into cluster2..." echo \ - ghcr.io/spiffe/spire-server:1.2.3 \ - ghcr.io/spiffe/spire-agent:1.2.3 \ + ghcr.io/spiffe/spire-server:1.7.0 \ + ghcr.io/spiffe/spire-agent:1.7.0 \ ghcr.io/spiffe/spiffe-csi-driver:0.2.0 \ ghcr.io/spiffe/spire-controller-manager:nightly \ greeter-client:demo \ @@ -139,6 +139,13 @@ log-info "Configuring the greeter server ID in cluster1..." log-info "Configuring the greeter client ID in cluster2..." ./cluster2 kubectl apply -f config/greeter-client-id.yaml +############################################################################ +# Add a static entry +############################################################################ + +log-info "Configuring the static entry in cluster1..." +./cluster1 kubectl apply -f config/static-entry.yaml + ############################################################################ # Check status ############################################################################ @@ -172,4 +179,19 @@ if [ -z "$SUCCESS" ]; then fail-now "Client never received response from server :(" fi +log-info "Checking for the static entry..." +SUCCESS= +for ((i = 0; i < 30; i++)); do + if ./cluster1 scripts/show-spire-entries.sh | grep -q static-spiffe-id; then + log-info "Static entry created in cluster1" + SUCCESS=true + break + fi + sleep 1 +done +if [ -z "$SUCCESS" ]; then + fail-now "Static entry never created :(" +fi + + log-good "Success." diff --git a/docs/clusterstaticentry-crd.md b/docs/clusterstaticentry-crd.md new file mode 100644 index 00000000..74944de7 --- /dev/null +++ b/docs/clusterstaticentry-crd.md @@ -0,0 +1,30 @@ +# ClusterStaticEntry Custom Resource Definition + +The ClusterStaticEntry Custom Resource Definition (CRD) is a cluster-wide +resource used to automate the registration of workloads that aren't running +within the Kubernetes cluster. + +The definition can be found [here](../api/v1alpha1/clusterstaticentry_types.go). + +## ClusterStaticEntrySpec + +| Field | Required | Description | +| ----- | -------- | ----------- | +| `spiffeID` | REQUIRED | The SPIFFE ID of the workload or node alias | +| `parentID` | REQUIRED | The parent ID of the node or nodes authorized for the entry or the SPIRE server ID for a node alias | +| `selectors` | REQUIRED | One or more workload selectors (when registering a workload) or node selectors (when registering a node alias) | +| `federatesWith` | OPTIONAL | One or more trust domain names that target workloads federate with | +| `x509SVIDTTL` | OPTIONAL | Duration value indicating an upper bound on the time-to-live for X509-SVIDs issued to target workload | +| `jwtSVIDTTL` | OPTIONAL | Duration value indicating an upper bound on the time-to-live for JWT-SVIDs issued to target workload | +| `dnsNames` | OPTIONAL | One or more DNS names for the target workload | +| `hint` | OPTIONAL | An opaque string that is provided to the workload as a hint on how the SVID should be used | +| `admin` | OPTIONAL | Indicates whether the target workload is an admin workload (i.e. can access SPIRE administrative APIs) | +| `downstream` | OPTIONAL | Indicates that the entry describes a downstream SPIRE server. | + +## ClusterStaticEntryStatus + +| Field | Description | +| ----- | ----------- | +| `rendered` | True if the cluster static entry was successfully rendered into a registration entry | +| `masked` | True if the entry produced by the cluster static entry was masked by another entry | +| `set` | True if the entry produced by the cluster static entry was successfully set on the SPIRE server | diff --git a/main.go b/main.go index d3e02250..ec17cbb0 100644 --- a/main.go +++ b/main.go @@ -278,6 +278,14 @@ func run(ctrlConfig spirev1alpha1.ControllerManagerConfig, options ctrl.Options) setupLog.Error(err, "unable to create controller", "controller", "ClusterFederatedTrustDomain") return err } + if err = (&controllers.ClusterStaticEntryReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Triggerer: entryReconciler, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ClusterStaticEntry") + return err + } if err = (&spirev1alpha1.ClusterFederatedTrustDomain{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "ClusterFederatedTrustDomain") return err diff --git a/pkg/k8sapi/helpers.go b/pkg/k8sapi/helpers.go index 36bf9a86..ba06c8b9 100644 --- a/pkg/k8sapi/helpers.go +++ b/pkg/k8sapi/helpers.go @@ -25,6 +25,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +func ListClusterStaticEntries(ctx context.Context, c client.Client) ([]spirev1alpha1.ClusterStaticEntry, error) { + var list spirev1alpha1.ClusterStaticEntryList + if err := c.List(ctx, &list); err != nil { + return nil, err + } + return list.Items, nil +} + func ListClusterSPIFFEIDs(ctx context.Context, c client.Client) ([]spirev1alpha1.ClusterSPIFFEID, error) { var list spirev1alpha1.ClusterSPIFFEIDList if err := c.List(ctx, &list); err != nil { diff --git a/pkg/spireapi/types.go b/pkg/spireapi/types.go index ba8df531..69b8af5b 100644 --- a/pkg/spireapi/types.go +++ b/pkg/spireapi/types.go @@ -38,10 +38,12 @@ type Entry struct { ParentID spiffeid.ID Selectors []Selector X509SVIDTTL time.Duration + JWTSVIDTTL time.Duration FederatesWith []spiffeid.TrustDomain Admin bool Downstream bool DNSNames []string + Hint string } type Selector struct { @@ -146,10 +148,12 @@ func entryToAPI(in Entry) *apitypes.Entry { ParentId: spiffeIDToAPI(in.ParentID), Selectors: selectorsToAPI(in.Selectors), X509SvidTtl: int32(in.X509SVIDTTL / time.Second), + JwtSvidTtl: int32(in.JWTSVIDTTL / time.Second), FederatesWith: trustDomainsToAPI(in.FederatesWith), Admin: in.Admin, DnsNames: in.DNSNames, Downstream: in.Downstream, + Hint: in.Hint, } } @@ -194,10 +198,12 @@ func entryFromAPI(in *apitypes.Entry) (Entry, error) { ParentID: parentID, Selectors: selectors, X509SVIDTTL: time.Duration(in.X509SvidTtl) * time.Second, + JWTSVIDTTL: time.Duration(in.JwtSvidTtl) * time.Second, FederatesWith: federatesWith, Admin: in.Admin, DNSNames: in.DnsNames, Downstream: in.Downstream, + Hint: in.Hint, }, nil } diff --git a/pkg/spireentry/by.go b/pkg/spireentry/by.go new file mode 100644 index 00000000..58a2e030 --- /dev/null +++ b/pkg/spireentry/by.go @@ -0,0 +1,60 @@ +package spireentry + +import ( + spirev1alpha1 "github.com/spiffe/spire-controller-manager/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" +) + +type byObject interface { + GetObjectKind() schema.ObjectKind + + GetUID() types.UID + GetCreationTimestamp() metav1.Time + GetDeletionTimestamp() *metav1.Time + + IncrementEntriesToSet() + IncrementEntriesMasked() + IncrementEntrySuccess() + IncrementEntryFailures() +} + +type ClusterStaticEntry struct { + spirev1alpha1.ClusterStaticEntry + NextStatus spirev1alpha1.ClusterStaticEntryStatus +} + +func (by *ClusterStaticEntry) IncrementEntriesToSet() { +} + +func (by *ClusterStaticEntry) IncrementEntriesMasked() { + by.NextStatus.Masked = true +} + +func (by *ClusterStaticEntry) IncrementEntrySuccess() { + by.NextStatus.Set = true +} + +func (by *ClusterStaticEntry) IncrementEntryFailures() { +} + +type ClusterSPIFFEID struct { + spirev1alpha1.ClusterSPIFFEID + NextStatus spirev1alpha1.ClusterSPIFFEIDStatus +} + +func (by *ClusterSPIFFEID) IncrementEntriesToSet() { + by.NextStatus.Stats.EntriesToSet++ +} + +func (by *ClusterSPIFFEID) IncrementEntriesMasked() { + by.NextStatus.Stats.EntriesMasked++ +} + +func (by *ClusterSPIFFEID) IncrementEntrySuccess() { +} + +func (by *ClusterSPIFFEID) IncrementEntryFailures() { + by.NextStatus.Stats.EntryFailures++ +} diff --git a/pkg/spireentry/entries.go b/pkg/spireentry/entries.go index 6e271a6e..17be1234 100644 --- a/pkg/spireentry/entries.go +++ b/pkg/spireentry/entries.go @@ -30,6 +30,41 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func renderStaticEntry(spec *spirev1alpha1.ClusterStaticEntrySpec) (*spireapi.Entry, error) { + spiffeID, err := spiffeid.FromString(spec.SPIFFEID) + if err != nil { + return nil, fmt.Errorf("failed to parse SPIFFEID: %w", err) + } + parentID, err := spiffeid.FromString(spec.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to parse ParentID: %w", err) + } + selectors, err := parseSelectors(spec.Selectors) + if err != nil { + return nil, fmt.Errorf("failed to parse Selectors: %w", err) + } + federatesWith := make([]spiffeid.TrustDomain, 0, len(spec.FederatesWith)) + for _, value := range spec.FederatesWith { + td, err := spiffeid.TrustDomainFromString(value) + if err != nil { + return nil, fmt.Errorf("invalid federatesWith value: %w", err) + } + federatesWith = append(federatesWith, td) + } + return &spireapi.Entry{ + SPIFFEID: spiffeID, + ParentID: parentID, + Selectors: selectors, + X509SVIDTTL: spec.X509SVIDTTL.Duration, + JWTSVIDTTL: spec.JWTSVIDTTL.Duration, + FederatesWith: federatesWith, + DNSNames: spec.DNSNames, + Admin: spec.Admin, + Downstream: spec.Downstream, + Hint: spec.Hint, + }, nil +} + func renderPodEntry(spec *spirev1alpha1.ParsedClusterSPIFFEIDSpec, node *corev1.Node, pod *corev1.Pod, trustDomain spiffeid.TrustDomain, clusterName, clusterDomain string) (*spireapi.Entry, error) { // We uniquely target the Pod running on the Node. The former is done // via the k8s:pod-uid selector, the latter via the parent ID. @@ -152,6 +187,18 @@ func validateDNSName(dnsName string) error { return nil } +func parseSelectors(selectors []string) ([]spireapi.Selector, error) { + ss := make([]spireapi.Selector, 0, len(selectors)) + for _, selector := range selectors { + s, err := parseSelector(selector) + if err != nil { + return nil, err + } + ss = append(ss, s) + } + return ss, nil +} + func parseSelector(selector string) (spireapi.Selector, error) { parts := strings.SplitN(selector, ":", 2) switch { diff --git a/pkg/spireentry/logging.go b/pkg/spireentry/logging.go index b0f0a124..ffebf236 100644 --- a/pkg/spireentry/logging.go +++ b/pkg/spireentry/logging.go @@ -27,17 +27,21 @@ import ( ) const ( - clusterSPIFFEIDLogKey = "clusterSPIFFEID" - namespaceLogKey = "namespace" - podLogKey = "pod" - idKey = "id" - parentIDKey = "parentID" - spiffeIDKey = "spiffeID" - selectorsKey = "selectors" - federatesWithKey = "federatesWith" - dnsNamesKey = "dnsNames" - adminKey = "admin" - downstreamKey = "downstream" + clusterStaticEntryLogKey = "clusterStaticEntry" + clusterSPIFFEIDLogKey = "clusterSPIFFEID" + namespaceLogKey = "namespace" + podLogKey = "pod" + idKey = "id" + parentIDKey = "parentID" + spiffeIDKey = "spiffeID" + selectorsKey = "selectors" + x509SVIDTTLKey = "x509SVIDTTL" + jwtSVIDTTLKey = "jwtSVIDTTL" + federatesWithKey = "federatesWith" + dnsNamesKey = "dnsNames" + adminKey = "admin" + downstreamKey = "downstream" + hintKey = "hint" ) func objectName(o metav1.Object) string { @@ -52,11 +56,14 @@ func entryLogFields(entry spireapi.Entry) []interface{} { idKey, entry.ID, parentIDKey, entry.ParentID.String(), spiffeIDKey, entry.SPIFFEID.String(), + x509SVIDTTLKey, entry.X509SVIDTTL.String(), + jwtSVIDTTLKey, entry.JWTSVIDTTL.String(), selectorsKey, stringFromSelectors(entry.Selectors), federatesWithKey, stringFromTrustDomains(entry.FederatesWith), dnsNamesKey, stringList(entry.DNSNames), adminKey, entry.Admin, downstreamKey, entry.Downstream, + hintKey, entry.Hint, } } diff --git a/pkg/spireentry/reconciler.go b/pkg/spireentry/reconciler.go index a857ba13..c8e58d56 100644 --- a/pkg/spireentry/reconciler.go +++ b/pkg/spireentry/reconciler.go @@ -83,67 +83,21 @@ func (r *entryReconciler) reconcile(ctx context.Context) { state.AddCurrent(entry) } - // Load ClusterSPIFFEIDs - clusterSPIFFEIDs, err := r.listClusterSPIFFEIDs(ctx) + // Load and add entry state for ClusterStaticEntries + clusterStaticEntries, err := r.listClusterStaticEntries(ctx) if err != nil { - log.Error(err, "Failed to list ClusterSPIFFEIDs") + log.Error(err, "Failed to list ClusterStaticEntries") return } + r.addClusterStaticEntryEntriesState(ctx, state, clusterStaticEntries) - // Build up the list of declared entries - for _, clusterSPIFFEID := range clusterSPIFFEIDs { - log := log.WithValues(clusterSPIFFEIDLogKey, objectName(clusterSPIFFEID)) - - spec, err := spirev1alpha1.ParseClusterSPIFFEIDSpec(&clusterSPIFFEID.Spec) - if err != nil { - // TODO: should this be prevented via admission webhook? should - // we dump this failure into the status? - log.Error(err, "Failed to parse ClusterSPIFFEID spec") - continue - } - - // List namespaces applicable to the ClusterSPIFFEID - namespaces, err := r.listNamespaces(ctx, spec.NamespaceSelector) - if err != nil { - log.Error(err, "Failed to list namespaces") - continue - } - - clusterSPIFFEID.NextStatus.Stats.NamespacesSelected += len(namespaces) - for i := range namespaces { - if r.config.IgnoreNamespaces.In(namespaces[i].Name) { - clusterSPIFFEID.NextStatus.Stats.NamespacesIgnored++ - continue - } - log := log.WithValues(namespaceLogKey, objectName(&namespaces[i])) - - pods, err := r.listNamespacePods(ctx, namespaces[i].Name, spec.PodSelector) - switch { - case err == nil: - case apierrors.IsNotFound(err): - continue - default: - log.Error(err, "Failed to list namespace pods") - continue - } - - clusterSPIFFEID.NextStatus.Stats.PodsSelected += len(pods) - for i := range pods { - log := log.WithValues(podLogKey, objectName(&pods[i])) - - entry, err := r.renderPodEntry(ctx, spec, &pods[i]) - switch { - case err != nil: - log.Error(err, "Failed to render entry") - clusterSPIFFEID.NextStatus.Stats.PodEntryRenderFailures++ - case entry != nil: - // renderPodEntry will return a nil entry if requisite k8s - // objects disappeared from underneath. - state.AddDeclared(*entry, clusterSPIFFEID) - } - } - } + // Load and add entry state for ClusterSPIFFEIDs + clusterSPIFFEIDs, err := r.listClusterSPIFFEIDs(ctx) + if err != nil { + log.Error(err, "Failed to list ClusterSPIFFEIDs") + return } + r.addClusterSPIFFEIDEntriesState(ctx, state, clusterSPIFFEIDs) var toDelete []spireapi.Entry var toCreate []declaredEntry @@ -155,11 +109,11 @@ func (r *entryReconciler) reconcile(ctx context.Context) { if len(s.Declared) > 0 { // Grab the first to set. preferredEntry := s.Declared[0] - preferredEntry.By.NextStatus.Stats.EntriesToSet++ + preferredEntry.By.IncrementEntriesToSet() // Record the remaining as masked. for _, otherEntry := range s.Declared[1:] { - otherEntry.By.NextStatus.Stats.EntriesMasked++ + otherEntry.By.IncrementEntriesMasked() } // Borrow the current entry ID if available, for the update. Then @@ -192,6 +146,21 @@ func (r *entryReconciler) reconcile(ctx context.Context) { r.updateEntries(ctx, toUpdate) } + // Update the ClusterStaticEntry statuses + for _, clusterStaticEntry := range clusterStaticEntries { + log := log.WithValues(clusterStaticEntryLogKey, objectName(clusterStaticEntry)) + + if clusterStaticEntry.Status == clusterStaticEntry.NextStatus { + continue + } + clusterStaticEntry.Status = clusterStaticEntry.NextStatus + if err := r.config.K8sClient.Status().Update(ctx, &clusterStaticEntry.ClusterStaticEntry); err == nil { + log.Info("Updated status") + } else { + log.Error(err, "Failed to update status") + } + } + // Update the ClusterSPIFFEID statuses for _, clusterSPIFFEID := range clusterSPIFFEIDs { log := log.WithValues(clusterSPIFFEIDLogKey, objectName(clusterSPIFFEID)) @@ -213,6 +182,20 @@ func (r *entryReconciler) listEntries(ctx context.Context) ([]spireapi.Entry, er return r.config.EntryClient.ListEntries(ctx) } +func (r *entryReconciler) listClusterStaticEntries(ctx context.Context) ([]*ClusterStaticEntry, error) { + clusterStaticEntries, err := k8sapi.ListClusterStaticEntries(ctx, r.config.K8sClient) + if err != nil { + return nil, err + } + out := make([]*ClusterStaticEntry, 0, len(clusterStaticEntries)) + for _, clusterStaticEntry := range clusterStaticEntries { + out = append(out, &ClusterStaticEntry{ + ClusterStaticEntry: clusterStaticEntry, + }) + } + return out, nil +} + func (r *entryReconciler) listClusterSPIFFEIDs(ctx context.Context) ([]*ClusterSPIFFEID, error) { clusterSPIFFEIDs, err := k8sapi.ListClusterSPIFFEIDs(ctx, r.config.K8sClient) if err != nil { @@ -235,6 +218,78 @@ func (r *entryReconciler) listNamespacePods(ctx context.Context, namespace strin return k8sapi.ListNamespacePods(ctx, r.config.K8sClient, namespace, podSelector) } +func (r *entryReconciler) addClusterStaticEntryEntriesState(ctx context.Context, state entriesState, clusterStaticEntries []*ClusterStaticEntry) { + log := log.FromContext(ctx) + for _, clusterStaticEntry := range clusterStaticEntries { + log := log.WithValues(clusterSPIFFEIDLogKey, objectName(clusterStaticEntry)) + entry, err := renderStaticEntry(&clusterStaticEntry.Spec) + if err != nil { + log.Error(err, "Failed to render ClusterStaticEntry") + clusterStaticEntry.NextStatus.Rendered = false + continue + } + clusterStaticEntry.NextStatus.Rendered = true + state.AddDeclared(*entry, clusterStaticEntry) + } +} + +func (r *entryReconciler) addClusterSPIFFEIDEntriesState(ctx context.Context, state entriesState, clusterSPIFFEIDs []*ClusterSPIFFEID) { + log := log.FromContext(ctx) + for _, clusterSPIFFEID := range clusterSPIFFEIDs { + log := log.WithValues(clusterSPIFFEIDLogKey, objectName(clusterSPIFFEID)) + + spec, err := spirev1alpha1.ParseClusterSPIFFEIDSpec(&clusterSPIFFEID.Spec) + if err != nil { + // TODO: should this be prevented via admission webhook? should + // we dump this failure into the status? + log.Error(err, "Failed to parse ClusterSPIFFEID spec") + continue + } + + // List namespaces applicable to the ClusterSPIFFEID + namespaces, err := r.listNamespaces(ctx, spec.NamespaceSelector) + if err != nil { + log.Error(err, "Failed to list namespaces") + continue + } + + clusterSPIFFEID.NextStatus.Stats.NamespacesSelected += len(namespaces) + for i := range namespaces { + if r.config.IgnoreNamespaces.In(namespaces[i].Name) { + clusterSPIFFEID.NextStatus.Stats.NamespacesIgnored++ + continue + } + log := log.WithValues(namespaceLogKey, objectName(&namespaces[i])) + + pods, err := r.listNamespacePods(ctx, namespaces[i].Name, spec.PodSelector) + switch { + case err == nil: + case apierrors.IsNotFound(err): + continue + default: + log.Error(err, "Failed to list namespace pods") + continue + } + + clusterSPIFFEID.NextStatus.Stats.PodsSelected += len(pods) + for i := range pods { + log := log.WithValues(podLogKey, objectName(&pods[i])) + + entry, err := r.renderPodEntry(ctx, spec, &pods[i]) + switch { + case err != nil: + log.Error(err, "Failed to render entry") + clusterSPIFFEID.NextStatus.Stats.PodEntryRenderFailures++ + case entry != nil: + // renderPodEntry will return a nil entry if requisite k8s + // objects disappeared from underneath. + state.AddDeclared(*entry, clusterSPIFFEID) + } + } + } + } +} + func (r *entryReconciler) renderPodEntry(ctx context.Context, spec *spirev1alpha1.ParsedClusterSPIFFEIDSpec, pod *corev1.Pod) (*spireapi.Entry, error) { // TODO: should we be caching this? probably not since it grabs from the // controller client, which is cached already. @@ -250,7 +305,7 @@ func (r *entryReconciler) createEntries(ctx context.Context, declaredEntries []d statuses, err := r.config.EntryClient.CreateEntries(ctx, entriesFromDeclaredEntries(declaredEntries)) if err != nil { for _, declaredEntry := range declaredEntries { - declaredEntry.By.NextStatus.Stats.EntryFailures++ + declaredEntry.By.IncrementEntryFailures() } log.Error(err, "Failed to update entries") return @@ -259,8 +314,9 @@ func (r *entryReconciler) createEntries(ctx context.Context, declaredEntries []d switch status.Code { case codes.OK: log.Info("Created entry", entryLogFields(declaredEntries[i].Entry)...) + declaredEntries[i].By.IncrementEntrySuccess() default: - declaredEntries[i].By.NextStatus.Stats.EntryFailures++ + declaredEntries[i].By.IncrementEntryFailures() log.Error(status.Err(), "Failed to create entry", entryLogFields(declaredEntries[i].Entry)...) } } @@ -271,7 +327,7 @@ func (r *entryReconciler) updateEntries(ctx context.Context, declaredEntries []d statuses, err := r.config.EntryClient.UpdateEntries(ctx, entriesFromDeclaredEntries(declaredEntries)) if err != nil { for _, declaredEntry := range declaredEntries { - declaredEntry.By.NextStatus.Stats.EntryFailures++ + declaredEntry.By.IncrementEntryFailures() } log.Error(err, "Failed to update entries") return @@ -281,7 +337,7 @@ func (r *entryReconciler) updateEntries(ctx context.Context, declaredEntries []d case codes.OK: log.Info("Updated entry", entryLogFields(declaredEntries[i].Entry)...) default: - declaredEntries[i].By.NextStatus.Stats.EntryFailures++ + declaredEntries[i].By.IncrementEntryFailures() log.Error(status.Err(), "Failed to update entry", entryLogFields(declaredEntries[i].Entry)...) } } @@ -311,11 +367,11 @@ func (es entriesState) AddCurrent(entry spireapi.Entry) { s.Current = append(s.Current, entry) } -func (es entriesState) AddDeclared(entry spireapi.Entry, source *ClusterSPIFFEID) { +func (es entriesState) AddDeclared(entry spireapi.Entry, by byObject) { s := es.stateFor(entry) s.Declared = append(s.Declared, declaredEntry{ Entry: entry, - By: source, + By: by, }) } @@ -329,11 +385,6 @@ func (es entriesState) stateFor(entry spireapi.Entry) *entryState { return s } -type ClusterSPIFFEID struct { - spirev1alpha1.ClusterSPIFFEID - NextStatus spirev1alpha1.ClusterSPIFFEIDStatus -} - type entryState struct { Current []spireapi.Entry Declared []declaredEntry @@ -341,7 +392,7 @@ type entryState struct { type declaredEntry struct { Entry spireapi.Entry - By *ClusterSPIFFEID + By byObject } type entryKey string @@ -376,41 +427,50 @@ func sortSelectors(unsorted []spireapi.Selector) []spireapi.Selector { func sortDeclaredEntriesByPreference(entries []declaredEntry) { // The most preferred is sorted to the first slot. sort.Slice(entries, func(i, j int) bool { - a := entries[i].By.ObjectMeta - b := entries[j].By.ObjectMeta + a, b := entries[i].By, entries[j].By + return objectCmp(a, b) < 0 + }) +} - // Sort ascending by creation timestamp - creationDiff := a.CreationTimestamp.UnixNano() - b.CreationTimestamp.UnixNano() +func objectCmp(a, b byObject) int { + // Sort ascending by creation timestamp + creationDiff := a.GetCreationTimestamp().UnixNano() - b.GetCreationTimestamp().UnixNano() + switch { + case creationDiff < 0: + return -1 + case creationDiff > 0: + return 1 + } + + // Sort _descending_ by deletion timestamp (those with no timestamp sort first) + switch { + case a.GetDeletionTimestamp() == nil && b.GetDeletionTimestamp() == nil: + // fallthrough to next criteria + case a.GetDeletionTimestamp() != nil && b.GetDeletionTimestamp() == nil: + return 1 + case a.GetDeletionTimestamp() == nil && b.GetDeletionTimestamp() != nil: + return -1 + case a.GetDeletionTimestamp() != nil && b.GetDeletionTimestamp() != nil: + deleteDiff := a.GetDeletionTimestamp().UnixNano() - b.GetDeletionTimestamp().UnixNano() switch { - case creationDiff < 0: - return true - case creationDiff > 0: - return false - } - - // Sort _descending_ by deletion timestamp (those with no timestamp sort first) - switch { - case a.DeletionTimestamp == nil && b.DeletionTimestamp == nil: - // fallthrough to next criteria - case a.DeletionTimestamp != nil && b.DeletionTimestamp == nil: - return false - case a.DeletionTimestamp == nil && b.DeletionTimestamp != nil: - return true - case a.DeletionTimestamp != nil && b.DeletionTimestamp != nil: - deleteDiff := a.DeletionTimestamp.UnixNano() - b.DeletionTimestamp.UnixNano() - switch { - case deleteDiff < 0: - return false - case deleteDiff > 0: - return true - } + case deleteDiff < 0: + return 1 + case deleteDiff > 0: + return -1 } + } - // At this point, these two entries are more or less equal in - // precedence, but we need a stable sorting mechanism, so tie-break - // with the UID. - return a.UID < b.UID - }) + // At this point, these two entries are more or less equal in + // precedence, but we need a stable sorting mechanism, so tie-break + // with the UID. + switch { + case a.GetUID() < b.GetUID(): + return -1 + case a.GetUID() > b.GetUID(): + return 1 + default: + return 0 + } } func getOutdatedEntryFields(newEntry, oldEntry spireapi.Entry) []string { @@ -421,6 +481,9 @@ func getOutdatedEntryFields(newEntry, oldEntry spireapi.Entry) []string { if oldEntry.X509SVIDTTL != newEntry.X509SVIDTTL { outdated = append(outdated, "x509SVIDTTL") } + if oldEntry.JWTSVIDTTL != newEntry.JWTSVIDTTL { + outdated = append(outdated, "jwtSVIDTTL") + } if !trustDomainsMatch(oldEntry.FederatesWith, newEntry.FederatesWith) { outdated = append(outdated, "federatesWith") } @@ -433,6 +496,9 @@ func getOutdatedEntryFields(newEntry, oldEntry spireapi.Entry) []string { if !stringsMatch(oldEntry.DNSNames, newEntry.DNSNames) { outdated = append(outdated, "dnsNames") } + if oldEntry.Hint != newEntry.Hint { + outdated = append(outdated, "hint") + } return outdated }