diff --git a/api/v1/istiorevisiontags_types.go b/api/v1/istiorevisiontags_types.go index 39bea8290..b20caa6e4 100644 --- a/api/v1/istiorevisiontags_types.go +++ b/api/v1/istiorevisiontags_types.go @@ -28,11 +28,11 @@ const ( // IstioRevisionTagSpec defines the desired state of IstioRevisionTag type IstioRevisionTagSpec struct { // +kubebuilder:validation:Required - TargetRef IstioRevisionTagTargetReference `json:"targetRef"` + TargetRef TargetReference `json:"targetRef"` } -// IstioRevisionTagTargetReference can reference either Istio or IstioRevision objects in the cluster. In the case of referencing an Istio object, the Sail Operator will automatically update the reference to the Istio object's Active Revision. -type IstioRevisionTagTargetReference struct { +// TargetReference can reference either Istio or IstioRevision objects in the cluster. In the case of referencing an Istio object, the Sail Operator will automatically update the reference to the Istio object's Active Revision. +type TargetReference struct { // Kind is the kind of the target resource. // // +kubebuilder:validation:Enum=Istio;IstioRevision diff --git a/api/v1/ztunnel_types.go b/api/v1/ztunnel_types.go index 3bbf7d229..050fc1250 100644 --- a/api/v1/ztunnel_types.go +++ b/api/v1/ztunnel_types.go @@ -42,6 +42,10 @@ type ZTunnelSpec struct { // Defines the values to be passed to the Helm charts when installing Istio ztunnel. // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Helm Values" Values *ZTunnelValues `json:"values,omitempty"` + + // The Istio control plane that this ZTunnel instance is associated with. Valid references are Istio and IstioRevision resources, Istio resources are always resolved to their current active revision. + // Values relevant for ZTunnel will be copied from the referenced IstioRevision resource, these are `spec.values.global`, `spec.values.meshConfig`, `spec.values.revision`. Any user configuration in the ZTunnel spec will always take precedence over the settings copied from the Istio resource, however. + TargetRef *TargetReference `json:"targetRef,omitempty"` } // ZTunnelStatus defines the observed state of ZTunnel @@ -57,6 +61,9 @@ type ZTunnelStatus struct { // Reports the current state of the object. State ZTunnelConditionReason `json:"state,omitempty"` + + // IstioRevision stores the name of the referenced IstioRevision + IstioRevision string `json:"istioRevision,omitempty"` } // GetCondition returns the condition of the specified type @@ -163,6 +170,7 @@ const ( // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Whether the Istio ztunnel installation is ready to handle requests." // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.state",description="The current state of this object." // +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version",description="The version of the Istio ztunnel installation." +// +kubebuilder:printcolumn:name="Revision",type="string",JSONPath=".status.istioRevision",description="The referenced IstioRevision." // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="The age of the object" // +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="metadata.name must be 'default'" diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 9445b1b09..fa6d7a69c 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -1429,21 +1429,6 @@ func (in *IstioRevisionTagStatus) DeepCopy() *IstioRevisionTagStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IstioRevisionTagTargetReference) DeepCopyInto(out *IstioRevisionTagTargetReference) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IstioRevisionTagTargetReference. -func (in *IstioRevisionTagTargetReference) DeepCopy() *IstioRevisionTagTargetReference { - if in == nil { - return nil - } - out := new(IstioRevisionTagTargetReference) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IstioSpec) DeepCopyInto(out *IstioSpec) { *out = *in @@ -4843,6 +4828,21 @@ func (in *StartupProbe) DeepCopy() *StartupProbe { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TargetReference) DeepCopyInto(out *TargetReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetReference. +func (in *TargetReference) DeepCopy() *TargetReference { + if in == nil { + return nil + } + out := new(TargetReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TargetUtilizationConfig) DeepCopyInto(out *TargetUtilizationConfig) { *out = *in @@ -5929,6 +5929,11 @@ func (in *ZTunnelSpec) DeepCopyInto(out *ZTunnelSpec) { *out = new(ZTunnelValues) (*in).DeepCopyInto(*out) } + if in.TargetRef != nil { + in, out := &in.TargetRef, &out.TargetRef + *out = new(TargetReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZTunnelSpec. diff --git a/bundle/manifests/sailoperator.clusterserviceversion.yaml b/bundle/manifests/sailoperator.clusterserviceversion.yaml index d3e6f5b4c..04c266dbc 100644 --- a/bundle/manifests/sailoperator.clusterserviceversion.yaml +++ b/bundle/manifests/sailoperator.clusterserviceversion.yaml @@ -45,7 +45,7 @@ metadata: capabilities: Seamless Upgrades categories: OpenShift Optional, Integration & Delivery, Networking, Security containerImage: quay.io/sail-dev/sail-operator:1.30-latest - createdAt: "2026-04-16T12:47:34Z" + createdAt: "2026-04-16T13:45:36Z" description: The Sail Operator manages the lifecycle of your Istio control plane. It provides custom resources for you to deploy and manage your control plane components. features.operators.openshift.io/cnf: "false" features.operators.openshift.io/cni: "true" diff --git a/bundle/manifests/sailoperator.io_istiorevisiontags.yaml b/bundle/manifests/sailoperator.io_istiorevisiontags.yaml index 65062f336..88f34a23b 100644 --- a/bundle/manifests/sailoperator.io_istiorevisiontags.yaml +++ b/bundle/manifests/sailoperator.io_istiorevisiontags.yaml @@ -64,10 +64,10 @@ spec: description: IstioRevisionTagSpec defines the desired state of IstioRevisionTag properties: targetRef: - description: IstioRevisionTagTargetReference can reference either - Istio or IstioRevision objects in the cluster. In the case of referencing - an Istio object, the Sail Operator will automatically update the - reference to the Istio object's Active Revision. + description: TargetReference can reference either Istio or IstioRevision + objects in the cluster. In the case of referencing an Istio object, + the Sail Operator will automatically update the reference to the + Istio object's Active Revision. properties: kind: description: Kind is the kind of the target resource. diff --git a/bundle/manifests/sailoperator.io_ztunnels.yaml b/bundle/manifests/sailoperator.io_ztunnels.yaml index 021a84613..f05066452 100644 --- a/bundle/manifests/sailoperator.io_ztunnels.yaml +++ b/bundle/manifests/sailoperator.io_ztunnels.yaml @@ -33,6 +33,10 @@ spec: jsonPath: .spec.version name: Version type: string + - description: The referenced IstioRevision. + jsonPath: .status.istioRevision + name: Revision + type: string - description: The age of the object jsonPath: .metadata.creationTimestamp name: Age @@ -70,6 +74,26 @@ spec: description: Namespace to which the Istio ztunnel component should be installed. type: string + targetRef: + description: |- + The Istio control plane that this ZTunnel instance is associated with. Valid references are Istio and IstioRevision resources, Istio resources are always resolved to their current active revision. + Values relevant for ZTunnel will be copied from the referenced IstioRevision resource, these are `spec.values.global`, `spec.values.meshConfig`, `spec.values.revision`. Any user configuration in the ZTunnel spec will always take precedence over the settings copied from the Istio resource, however. + properties: + kind: + description: Kind is the kind of the target resource. + enum: + - Istio + - IstioRevision + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + required: + - kind + - name + type: object values: description: Defines the values to be passed to the Helm charts when installing Istio ztunnel. @@ -3600,6 +3624,9 @@ spec: type: string type: object type: array + istioRevision: + description: IstioRevision stores the name of the referenced IstioRevision + type: string observedGeneration: description: |- ObservedGeneration is the most recent generation observed for this diff --git a/chart/crds/sailoperator.io_istiorevisiontags.yaml b/chart/crds/sailoperator.io_istiorevisiontags.yaml index 7ba6c9540..7731a5411 100644 --- a/chart/crds/sailoperator.io_istiorevisiontags.yaml +++ b/chart/crds/sailoperator.io_istiorevisiontags.yaml @@ -64,10 +64,10 @@ spec: description: IstioRevisionTagSpec defines the desired state of IstioRevisionTag properties: targetRef: - description: IstioRevisionTagTargetReference can reference either - Istio or IstioRevision objects in the cluster. In the case of referencing - an Istio object, the Sail Operator will automatically update the - reference to the Istio object's Active Revision. + description: TargetReference can reference either Istio or IstioRevision + objects in the cluster. In the case of referencing an Istio object, + the Sail Operator will automatically update the reference to the + Istio object's Active Revision. properties: kind: description: Kind is the kind of the target resource. diff --git a/chart/crds/sailoperator.io_ztunnels.yaml b/chart/crds/sailoperator.io_ztunnels.yaml index b6243dcf8..6385dd665 100644 --- a/chart/crds/sailoperator.io_ztunnels.yaml +++ b/chart/crds/sailoperator.io_ztunnels.yaml @@ -33,6 +33,10 @@ spec: jsonPath: .spec.version name: Version type: string + - description: The referenced IstioRevision. + jsonPath: .status.istioRevision + name: Revision + type: string - description: The age of the object jsonPath: .metadata.creationTimestamp name: Age @@ -70,6 +74,26 @@ spec: description: Namespace to which the Istio ztunnel component should be installed. type: string + targetRef: + description: |- + The Istio control plane that this ZTunnel instance is associated with. Valid references are Istio and IstioRevision resources, Istio resources are always resolved to their current active revision. + Values relevant for ZTunnel will be copied from the referenced IstioRevision resource, these are `spec.values.global`, `spec.values.meshConfig`, `spec.values.revision`. Any user configuration in the ZTunnel spec will always take precedence over the settings copied from the Istio resource, however. + properties: + kind: + description: Kind is the kind of the target resource. + enum: + - Istio + - IstioRevision + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + required: + - kind + - name + type: object values: description: Defines the values to be passed to the Helm charts when installing Istio ztunnel. @@ -3600,6 +3624,9 @@ spec: type: string type: object type: array + istioRevision: + description: IstioRevision stores the name of the referenced IstioRevision + type: string observedGeneration: description: |- ObservedGeneration is the most recent generation observed for this diff --git a/chart/samples/ambient/istioztunnel-sample.yaml b/chart/samples/ambient/istioztunnel-sample.yaml index 4fc29de3a..ebb668d38 100644 --- a/chart/samples/ambient/istioztunnel-sample.yaml +++ b/chart/samples/ambient/istioztunnel-sample.yaml @@ -5,3 +5,6 @@ metadata: spec: version: v1.29.2 namespace: ztunnel + targetRef: + kind: Istio + name: default diff --git a/controllers/istiorevisiontag/istiorevisiontag_controller.go b/controllers/istiorevisiontag/istiorevisiontag_controller.go index b03b70e30..2e695b2e0 100644 --- a/controllers/istiorevisiontag/istiorevisiontag_controller.go +++ b/controllers/istiorevisiontag/istiorevisiontag_controller.go @@ -102,7 +102,7 @@ func (r *Reconciler) doReconcile(ctx context.Context, tag *v1.IstioRevisionTag) } log.Info("Retrieving referenced IstioRevision for IstioRevisionTag") - rev, err := r.getIstioRevision(ctx, tag.Spec.TargetRef) + rev, err := revision.GetIstioRevisionFromTargetReference(ctx, r.Client, tag.Spec.TargetRef) if rev == nil || err != nil { return nil, err } @@ -157,32 +157,6 @@ func (r *Reconciler) validate(ctx context.Context, tag *v1.IstioRevisionTag) err return nil } -func (r *Reconciler) getIstioRevision(ctx context.Context, ref v1.IstioRevisionTagTargetReference) (*v1.IstioRevision, error) { - var revisionName string - if ref.Kind == v1.IstioRevisionKind { - revisionName = ref.Name - } else if ref.Kind == v1.IstioKind { - i := v1.Istio{} - err := r.Client.Get(ctx, types.NamespacedName{Name: ref.Name}, &i) - if err != nil { - return nil, err - } - if i.Status.ActiveRevisionName == "" { - return nil, reconciler.NewTransientError("referenced Istio has no active revision") - } - revisionName = i.Status.ActiveRevisionName - } else { - return nil, reconciler.NewValidationError("unknown targetRef.kind") - } - - rev := v1.IstioRevision{} - err := r.Client.Get(ctx, types.NamespacedName{Name: revisionName}, &rev) - if err != nil { - return nil, err - } - return &rev, nil -} - func (r *Reconciler) installHelmCharts(ctx context.Context, tag *v1.IstioRevisionTag, rev *v1.IstioRevision) error { ownerReference := metav1.OwnerReference{ APIVersion: v1.GroupVersion.String(), @@ -401,7 +375,7 @@ func (r *Reconciler) isRevisionTagReferencedByWorkloads(ctx context.Context, tag } } - rev, err := r.getIstioRevision(ctx, tag.Spec.TargetRef) + rev, err := revision.GetIstioRevisionFromTargetReference(ctx, r.Client, tag.Spec.TargetRef) if err != nil { return false, err } diff --git a/controllers/istiorevisiontag/istiorevisiontag_controller_test.go b/controllers/istiorevisiontag/istiorevisiontag_controller_test.go index 88026c713..3d01911f6 100644 --- a/controllers/istiorevisiontag/istiorevisiontag_controller_test.go +++ b/controllers/istiorevisiontag/istiorevisiontag_controller_test.go @@ -219,7 +219,7 @@ func TestDetermineInUseCondition(t *testing.T) { Name: tagName, }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "IstioRevision", Name: rev.Name, }, @@ -300,7 +300,7 @@ func TestValidation(t *testing.T) { Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{}, + TargetRef: v1.TargetReference{}, }, }, expectedErrMessage: "spec.targetRef not set", @@ -313,7 +313,7 @@ func TestValidation(t *testing.T) { Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "IstioRevision", Name: revName, }, diff --git a/controllers/ztunnel/ztunnel_controller.go b/controllers/ztunnel/ztunnel_controller.go index 26591feba..13da232e9 100644 --- a/controllers/ztunnel/ztunnel_controller.go +++ b/controllers/ztunnel/ztunnel_controller.go @@ -32,6 +32,7 @@ import ( "github.com/istio-ecosystem/sail-operator/pkg/predicate" sharedreconcile "github.com/istio-ecosystem/sail-operator/pkg/reconcile" "github.com/istio-ecosystem/sail-operator/pkg/reconciler" + "github.com/istio-ecosystem/sail-operator/pkg/revision" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -84,10 +85,10 @@ func NewReconciler(cfg config.ReconcilerConfig, client client.Client, scheme *ru func (r *Reconciler) Reconcile(ctx context.Context, ztunnel *v1.ZTunnel) (ctrl.Result, error) { log := logf.FromContext(ctx) - reconcileErr := r.doReconcile(ctx, ztunnel) + rev, reconcileErr := r.doReconcile(ctx, ztunnel) log.Info("Reconciliation done. Updating status.") - statusErr := r.updateStatus(ctx, ztunnel, reconcileErr) + statusErr := r.updateStatus(ctx, ztunnel, rev, reconcileErr) return ctrl.Result{}, errors.Join(reconcileErr, statusErr) } @@ -97,15 +98,29 @@ func (r *Reconciler) Finalize(ctx context.Context, ztunnel *v1.ZTunnel) error { return ztunnelReconciler.Uninstall(ctx, ztunnel.Spec.Namespace) } -func (r *Reconciler) doReconcile(ctx context.Context, ztunnel *v1.ZTunnel) error { +func (r *Reconciler) doReconcile(ctx context.Context, ztunnel *v1.ZTunnel) (rev *v1.IstioRevision, err error) { log := logf.FromContext(ctx) ztunnelReconciler := r.newZTunnelReconciler() if err := ztunnelReconciler.Validate(ctx, ztunnel.Spec.Version, ztunnel.Spec.Namespace); err != nil { - return err + return nil, err + } + + if ztunnel.Spec.TargetRef != nil { + log.Info("Retrieving referenced IstioRevision") + rev, err = revision.GetIstioRevisionFromTargetReference(ctx, r.Client, *ztunnel.Spec.TargetRef) + if err != nil { + return nil, err + } } log.Info("Installing ztunnel Helm chart") + return rev, r.installHelmChart(ctx, ztunnel, ztunnelReconciler, rev) +} + +func (r *Reconciler) installHelmChart(ctx context.Context, ztunnel *v1.ZTunnel, + ztunnelReconciler *sharedreconcile.ZTunnelReconciler, rev *v1.IstioRevision, +) error { ownerReference := metav1.OwnerReference{ APIVersion: v1.GroupVersion.String(), Kind: v1.ZTunnelKind, @@ -114,6 +129,17 @@ func (r *Reconciler) doReconcile(ctx context.Context, ztunnel *v1.ZTunnel) error Controller: ptr.Of(true), BlockOwnerDeletion: ptr.Of(true), } + + if rev != nil && rev.Spec.Values != nil { + revisionValues := helm.FromValues(v1.Values{ + MeshConfig: rev.Spec.Values.MeshConfig, + Revision: rev.Spec.Values.Revision, + Global: rev.Spec.Values.Global, + }) + return ztunnelReconciler.Install( + ctx, ztunnel.Spec.Version, ztunnel.Spec.Namespace, ztunnel.Spec.Values, &ownerReference, revisionValues) + } + return ztunnelReconciler.Install(ctx, ztunnel.Spec.Version, ztunnel.Spec.Namespace, ztunnel.Spec.Values, &ownerReference) } @@ -134,6 +160,9 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { // mainObjectHandler handles the ZTunnel watch events mainObjectHandler := wrapEventHandler(logger, &handler.EnqueueRequestForObject{}) + // operatorResourcesHandler handles watch events from operator CRDs Istio and IstioRevision + operatorResourcesHandler := wrapEventHandler(logger, handler.EnqueueRequestsFromMapFunc(r.mapOperatorResourceToReconcileRequest)) + // ownedResourceHandler handles resources that are owned by the ZTunnel CR ownedResourceHandler := wrapEventHandler(logger, handler.EnqueueRequestForOwner(r.Scheme, r.RESTMapper(), &v1.ZTunnel{}, handler.OnlyControllerOwner())) @@ -172,10 +201,12 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { Watches(&corev1.Namespace{}, namespaceHandler). Watches(&rbacv1.ClusterRole{}, ownedResourceHandler). Watches(&rbacv1.ClusterRoleBinding{}, ownedResourceHandler). + Watches(&v1.Istio{}, operatorResourcesHandler). + Watches(&v1.IstioRevision{}, operatorResourcesHandler). Complete(reconciler.NewStandardReconcilerWithFinalizer[*v1.ZTunnel](r.Client, r.Reconcile, r.Finalize, constants.FinalizerName)) } -func (r *Reconciler) determineStatus(ctx context.Context, ztunnel *v1.ZTunnel, reconcileErr error) (v1.ZTunnelStatus, error) { +func (r *Reconciler) determineStatus(ctx context.Context, ztunnel *v1.ZTunnel, rev *v1.IstioRevision, reconcileErr error) (v1.ZTunnelStatus, error) { var errs errlist.Builder reconciledCondition := r.determineReconciledCondition(reconcileErr) readyCondition, err := r.determineReadyCondition(ctx, ztunnel) @@ -186,13 +217,17 @@ func (r *Reconciler) determineStatus(ctx context.Context, ztunnel *v1.ZTunnel, r status.SetCondition(reconciledCondition) status.SetCondition(readyCondition) status.State = deriveState(reconciledCondition, readyCondition) + status.IstioRevision = "" + if rev != nil { + status.IstioRevision = rev.Name + } return status, errs.Error() } -func (r *Reconciler) updateStatus(ctx context.Context, ztunnel *v1.ZTunnel, reconcileErr error) error { +func (r *Reconciler) updateStatus(ctx context.Context, ztunnel *v1.ZTunnel, rev *v1.IstioRevision, reconcileErr error) error { var errs errlist.Builder - status, err := r.determineStatus(ctx, ztunnel, reconcileErr) + status, err := r.determineStatus(ctx, ztunnel, rev, reconcileErr) if err != nil { errs.Add(fmt.Errorf("failed to determine status: %w", err)) } @@ -282,6 +317,31 @@ func (r *Reconciler) mapNamespaceToReconcileRequest(ctx context.Context, ns clie return requests } +func (r *Reconciler) mapOperatorResourceToReconcileRequest(ctx context.Context, obj client.Object) []reconcile.Request { + log := logf.FromContext(ctx) + var revisionName string + if i, ok := obj.(*v1.Istio); ok && i.Status.ActiveRevisionName != "" { + revisionName = i.Status.ActiveRevisionName + } else if rev, ok := obj.(*v1.IstioRevision); ok { + revisionName = rev.Name + } else { + return nil + } + ztunnels := v1.ZTunnelList{} + err := r.Client.List(ctx, &ztunnels) + if err != nil { + log.Error(err, "failed to list ZTunnels") + return nil + } + requests := []reconcile.Request{} + for _, ztunnel := range ztunnels.Items { + if ztunnel.Status.IstioRevision == revisionName { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: ztunnel.Name}}) + } + } + return requests +} + func wrapEventHandler(logger logr.Logger, handler handler.EventHandler) handler.EventHandler { return enqueuelogger.WrapIfNecessary(v1.ZTunnelKind, logger, handler) } diff --git a/controllers/ztunnel/ztunnel_controller_test.go b/controllers/ztunnel/ztunnel_controller_test.go index c5d49e3dc..7fb07ae15 100644 --- a/controllers/ztunnel/ztunnel_controller_test.go +++ b/controllers/ztunnel/ztunnel_controller_test.go @@ -539,9 +539,15 @@ func TestDetermineStatus(t *testing.T) { tests := []struct { name string reconcileErr error + rev *v1.IstioRevision }{ { - name: "no error", + name: "no error", + rev: &v1.IstioRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, reconcileErr: nil, }, { @@ -565,7 +571,7 @@ func TestDetermineStatus(t *testing.T) { }, } - status, err := r.determineStatus(ctx, ztunnel, tt.reconcileErr) + status, err := r.determineStatus(ctx, ztunnel, tt.rev, tt.reconcileErr) g.Expect(err).ToNot(HaveOccurred()) g.Expect(status.ObservedGeneration).To(Equal(ztunnel.Generation)) @@ -577,6 +583,11 @@ func TestDetermineStatus(t *testing.T) { g.Expect(status.State).To(Equal(deriveState(reconciledCondition, readyCondition))) g.Expect(normalize(status.GetCondition(v1.ZTunnelConditionReconciled))).To(Equal(normalize(reconciledCondition))) g.Expect(normalize(status.GetCondition(v1.ZTunnelConditionReady))).To(Equal(normalize(readyCondition))) + if tt.rev != nil { + g.Expect(status.IstioRevision).To(Equal(tt.rev.Name)) + } else { + g.Expect(status.IstioRevision).To(BeEmpty()) + } }) } } diff --git a/docs/api-reference/sailoperator.io.md b/docs/api-reference/sailoperator.io.md index b809df644..80defd301 100644 --- a/docs/api-reference/sailoperator.io.md +++ b/docs/api-reference/sailoperator.io.md @@ -1060,7 +1060,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `targetRef` _[IstioRevisionTagTargetReference](#istiorevisiontagtargetreference)_ | | | Required: \{\} | +| `targetRef` _[TargetReference](#targetreference)_ | | | Required: \{\} | #### IstioRevisionTagStatus @@ -1083,23 +1083,6 @@ _Appears in:_ | `istioRevision` _string_ | IstioRevision stores the name of the referenced IstioRevision | | | -#### IstioRevisionTagTargetReference - - - -IstioRevisionTagTargetReference can reference either Istio or IstioRevision objects in the cluster. In the case of referencing an Istio object, the Sail Operator will automatically update the reference to the Istio object's Active Revision. - - - -_Appears in:_ -- [IstioRevisionTagSpec](#istiorevisiontagspec) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `kind` _string_ | Kind is the kind of the target resource. | | Enum: [Istio IstioRevision] Required: \{\} | -| `name` _string_ | Name is the name of the target resource. | | MaxLength: 253 MinLength: 1 Required: \{\} | - - #### IstioSpec @@ -2996,6 +2979,24 @@ _Appears in:_ | `failureThreshold` _integer_ | Minimum consecutive failures for the probe to be considered failed after having succeeded. | | | +#### TargetReference + + + +TargetReference can reference either Istio or IstioRevision objects in the cluster. In the case of referencing an Istio object, the Sail Operator will automatically update the reference to the Istio object's Active Revision. + + + +_Appears in:_ +- [IstioRevisionTagSpec](#istiorevisiontagspec) +- [ZTunnelSpec](#ztunnelspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `kind` _string_ | Kind is the kind of the target resource. | | Enum: [Istio IstioRevision] Required: \{\} | +| `name` _string_ | Name is the name of the target resource. | | MaxLength: 253 MinLength: 1 Required: \{\} | + + #### TargetUtilizationConfig @@ -3572,6 +3573,7 @@ _Appears in:_ | `version` _string_ | Defines the version of Istio to install. Must be one of: v1.29-latest, v1.29.2, v1.29.1, v1.29.0, v1.28-latest, v1.28.6, v1.28.5, v1.28.4, v1.28.3, v1.28.2, v1.28.1, v1.28.0, master, v1.30-alpha.e2413abb. | v1.29.2 | Enum: [v1.29-latest v1.29.2 v1.29.1 v1.29.0 v1.28-latest v1.28.6 v1.28.5 v1.28.4 v1.28.3 v1.28.2 v1.28.1 v1.28.0 v1.27-latest v1.27.9 v1.27.8 v1.27.7 v1.27.6 v1.27.5 v1.27.4 v1.27.3 v1.27.2 v1.27.1 v1.27.0 v1.26-latest v1.26.8 v1.26.7 v1.26.6 v1.26.5 v1.26.4 v1.26.3 v1.26.2 v1.26.1 v1.26.0 v1.25-latest v1.25.5 v1.25.4 v1.25.3 v1.25.2 v1.25.1 v1.24-latest v1.24.6 v1.24.5 v1.24.4 v1.24.3 v1.24.2 v1.24.1 v1.24.0 master v1.30-alpha.e2413abb] | | `namespace` _string_ | Namespace to which the Istio ztunnel component should be installed. | ztunnel | | | `values` _[ZTunnelValues](#ztunnelvalues)_ | Defines the values to be passed to the Helm charts when installing Istio ztunnel. | | | +| `targetRef` _[TargetReference](#targetreference)_ | The Istio control plane that this ZTunnel instance is associated with. Valid references are Istio and IstioRevision resources, Istio resources are always resolved to their current active revision. Values relevant for ZTunnel will be copied from the referenced IstioRevision resource, these are `spec.values.global`, `spec.values.meshConfig`, `spec.values.revision`. Any user configuration in the ZTunnel spec will always take precedence over the settings copied from the Istio resource, however. | | | #### ZTunnelStatus @@ -3590,6 +3592,7 @@ _Appears in:_ | `observedGeneration` _integer_ | ObservedGeneration is the most recent generation observed for this ZTunnel object. It corresponds to the object's generation, which is updated on mutation by the API Server. The information in the status pertains to this particular generation of the object. | | | | `conditions` _[ZTunnelCondition](#ztunnelcondition) array_ | Represents the latest available observations of the object's current state. | | | | `state` _[ZTunnelConditionReason](#ztunnelconditionreason)_ | Reports the current state of the object. | | | +| `istioRevision` _string_ | IstioRevision stores the name of the referenced IstioRevision | | | #### ZTunnelValues diff --git a/docs/common/istio-ambient-mode.adoc b/docs/common/istio-ambient-mode.adoc index 26fef0e9c..a92a76518 100644 --- a/docs/common/istio-ambient-mode.adoc +++ b/docs/common/istio-ambient-mode.adoc @@ -50,6 +50,8 @@ NOTE: The ZTunnel API was promoted from `v1alpha1` to `v1`. If you have existing The `ZTunnel` resource manages the L4 node proxy and is a cluster-wide resource. It deploys a DaemonSet that runs on all nodes in the cluster. You can specify the version using the `spec.version` field, as shown in the example below. Similar to the `Istio` resource, it also includes a `values` field that allows you to configure options available in the ztunnel helm chart. The `metadata.name` field must be set to `default`, as enforced by a CRD validation rule that guarantees only one `ZTunnel` instance exists cluster-wide. +The `spec.targetRef` field allows you to associate the ZTunnel instance with an Istio control plane. When set, values relevant for ZTunnel (such as `global`, `meshConfig`, and `revision`) are automatically copied from the referenced `Istio` or `IstioRevision` resource. Any values explicitly set in the ZTunnel's `spec.values` will take precedence over those inherited from the referenced resource. + [source,yaml] ---- apiVersion: sailoperator.io/v1 @@ -58,9 +60,9 @@ metadata: name: default spec: namespace: ztunnel - values: - ztunnel: - image: docker.io/istio/ztunnel:{istio_latest_version} + targetRef: + kind: Istio + name: default ---- NOTE: If you need a specific Istio version, you can explicitly set it using `spec.version`. If not specified, the Operator will install the latest supported version. @@ -175,6 +177,8 @@ kubectl label namespace ztunnel istio-discovery=enabled . Create the `ZTunnel` resource. + +NOTE: The `targetRef` field links the ZTunnel to the Istio control plane, so that relevant configuration values are automatically inherited. ++ [source,bash,subs="attributes+"] ---- cat < 0 && baseValues[0] != nil { + // Apply base values (from IstioRevision) on top of profile defaults, like an additional profile + mergedHelmValues, err = istiovalues.ApplyProfilesAndPlatform( + r.cfg.ResourceFS, resolvedVersion, r.cfg.Platform, r.cfg.DefaultProfile, ztunnelProfile, baseValues[0]) + if err != nil { + return nil, fmt.Errorf("failed to apply profile: %w", err) + } + + // Apply user values on top so they always take precedence + mergedHelmValues, err = istiovalues.ApplyUserValues(mergedHelmValues, helm.FromValues(userValues)) + if err != nil { + return nil, fmt.Errorf("failed to apply user values: %w", err) + } + } else { + // Apply userValues on top of defaultValues from profiles + mergedHelmValues, err = istiovalues.ApplyProfilesAndPlatform( + r.cfg.ResourceFS, resolvedVersion, r.cfg.Platform, r.cfg.DefaultProfile, ztunnelProfile, helm.FromValues(userValues)) + if err != nil { + return nil, fmt.Errorf("failed to apply profile: %w", err) + } } // Apply any user Overrides configured as part of values.ztunnel @@ -107,8 +125,12 @@ func (r *ZTunnelReconciler) ComputeValues(version string, userValues *v1.ZTunnel } // Install installs or upgrades the ztunnel Helm chart. -func (r *ZTunnelReconciler) Install(ctx context.Context, version, namespace string, values *v1.ZTunnelValues, ownerRef *metav1.OwnerReference) error { - finalHelmValues, err := r.ComputeValues(version, values) +// If baseValues are provided (e.g. from a referenced IstioRevision), they are passed to ComputeValues +// to be merged early in the pipeline, before profiles and FIPS values are applied. +func (r *ZTunnelReconciler) Install( + ctx context.Context, version, namespace string, values *v1.ZTunnelValues, ownerRef *metav1.OwnerReference, baseValues ...helm.Values, +) error { + finalHelmValues, err := r.ComputeValues(version, values, baseValues...) if err != nil { return err } diff --git a/pkg/revision/references.go b/pkg/revision/references.go index c84879d7c..bb565ebc0 100644 --- a/pkg/revision/references.go +++ b/pkg/revision/references.go @@ -15,8 +15,13 @@ package revision import ( + "context" + v1 "github.com/istio-ecosystem/sail-operator/api/v1" "github.com/istio-ecosystem/sail-operator/pkg/constants" + "github.com/istio-ecosystem/sail-operator/pkg/reconciler" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) func GetReferencedRevisionFromNamespace(labels map[string]string) string { @@ -46,3 +51,30 @@ func GetInjectedRevisionFromPod(podAnnotations map[string]string) string { // if pod was already injected, the revision that did the injection is specified in the istio.io/rev annotation return podAnnotations[constants.IstioRevLabel] } + +func GetIstioRevisionFromTargetReference(ctx context.Context, client client.Client, ref v1.TargetReference) (*v1.IstioRevision, error) { + var revisionName string + switch ref.Kind { + case v1.IstioRevisionKind: + revisionName = ref.Name + case v1.IstioKind: + i := v1.Istio{} + err := client.Get(ctx, types.NamespacedName{Name: ref.Name}, &i) + if err != nil { + return nil, err + } + if i.Status.ActiveRevisionName == "" { + return nil, reconciler.NewTransientError("referenced Istio has no active revision") + } + revisionName = i.Status.ActiveRevisionName + default: + return nil, reconciler.NewValidationError("unknown targetRef.kind") + } + + rev := v1.IstioRevision{} + err := client.Get(ctx, types.NamespacedName{Name: revisionName}, &rev) + if err != nil { + return nil, err + } + return &rev, nil +} diff --git a/pkg/revision/references_test.go b/pkg/revision/references_test.go index 0b4ed0550..7c6b7fee3 100644 --- a/pkg/revision/references_test.go +++ b/pkg/revision/references_test.go @@ -15,9 +15,15 @@ package revision import ( + "context" "testing" + v1 "github.com/istio-ecosystem/sail-operator/api/v1" + "github.com/istio-ecosystem/sail-operator/pkg/scheme" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestGetReferencedRevisionFromNamespace(t *testing.T) { @@ -145,3 +151,83 @@ func TestGetInjectedRevisionFromPod(t *testing.T) { }) } } + +func TestGetIstioRevisionFromTargetReference(t *testing.T) { + rev := &v1.IstioRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-revision", + }, + } + istioWithActiveRevision := &v1.Istio{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-istio", + }, + Status: v1.IstioStatus{ + ActiveRevisionName: "my-revision", + }, + } + istioWithoutActiveRevision := &v1.Istio{ + ObjectMeta: metav1.ObjectMeta{ + Name: "no-active", + }, + } + + tests := []struct { + name string + ref v1.TargetReference + objects []client.Object + expectedRev string + expectErr string + }{ + { + name: "IstioRevision reference", + ref: v1.TargetReference{Kind: v1.IstioRevisionKind, Name: "my-revision"}, + objects: []client.Object{rev}, + expectedRev: "my-revision", + }, + { + name: "Istio reference with active revision", + ref: v1.TargetReference{Kind: v1.IstioKind, Name: "my-istio"}, + objects: []client.Object{istioWithActiveRevision, rev}, + expectedRev: "my-revision", + }, + { + name: "Istio reference without active revision", + ref: v1.TargetReference{Kind: v1.IstioKind, Name: "no-active"}, + objects: []client.Object{istioWithoutActiveRevision}, + expectErr: "referenced Istio has no active revision", + }, + { + name: "Istio reference not found", + ref: v1.TargetReference{Kind: v1.IstioKind, Name: "nonexistent"}, + objects: []client.Object{}, + expectErr: "not found", + }, + { + name: "IstioRevision reference not found", + ref: v1.TargetReference{Kind: v1.IstioRevisionKind, Name: "nonexistent"}, + objects: []client.Object{}, + expectErr: "not found", + }, + { + name: "unknown kind", + ref: v1.TargetReference{Kind: "UnknownKind", Name: "test"}, + objects: []client.Object{}, + expectErr: "unknown targetRef.kind", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cl := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(tt.objects...).WithStatusSubresource(&v1.Istio{}).Build() + result, err := GetIstioRevisionFromTargetReference(context.TODO(), cl, tt.ref) + if tt.expectErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectErr) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedRev, result.Name) + } + }) + } +} diff --git a/tests/e2e/ambient/ambient_test.go b/tests/e2e/ambient/ambient_test.go index 6753cb948..30ec42d6e 100644 --- a/tests/e2e/ambient/ambient_test.go +++ b/tests/e2e/ambient/ambient_test.go @@ -115,6 +115,8 @@ spec: BeforeAll(func() { common.CreateIstio(k, version.Name, ` values: + global: + network: custom-network pilot: trustedZtunnelNamespace: ztunnel profile: ambient`) @@ -166,11 +168,14 @@ metadata: spec: version: %s namespace: %s + targetRef: + kind: Istio + name: %s values: ztunnel: env: CUSTOM_ENV_VAR: "true"` - ztunnelYaml = fmt.Sprintf(ztunnelYaml, version.Name, ztunnelNamespace) + ztunnelYaml = fmt.Sprintf(ztunnelYaml, version.Name, ztunnelNamespace, istioName) Log("ZTunnel YAML:", ztunnelYaml) Expect(k.CreateFromString(ztunnelYaml)).To(Succeed(), "ZTunnel creation failed") Success("ZTunnel created") @@ -206,6 +211,12 @@ spec: Expect(ztunnelObj).To(common.HaveContainersThat(ContainElement(WithTransform(getEnvVars, ContainElement(corev1.EnvVar{Name: "CUSTOM_ENV_VAR", Value: "true"})))), "Expected CUSTOM_ENV_VAR to be set to true, but not found") + + if version.Version.GreaterThanEqual(semver.New(1, 27, 0, "", "")) { + Expect(ztunnelObj).To(common.HaveContainersThat(ContainElement(WithTransform(getEnvVars, + ContainElement(corev1.EnvVar{Name: "NETWORK", Value: "custom-network"})))), + "Expected NETWORK to be set to custom-network, but not found") + } }) }) diff --git a/tests/integration/api/istiobase_test.go b/tests/integration/api/istiobase_test.go index 4aa1e9dd2..af5ef7931 100644 --- a/tests/integration/api/istiobase_test.go +++ b/tests/integration/api/istiobase_test.go @@ -129,7 +129,7 @@ var _ = Describe("base chart support", Ordered, func() { Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "IstioRevision", Name: "my-rev", }, @@ -322,7 +322,7 @@ var _ = Describe("base chart support", Ordered, func() { Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "IstioRevision", Name: "rev1", }, diff --git a/tests/integration/api/istiorevision_test.go b/tests/integration/api/istiorevision_test.go index a0c23b860..67084080e 100644 --- a/tests/integration/api/istiorevision_test.go +++ b/tests/integration/api/istiorevision_test.go @@ -909,7 +909,7 @@ var _ = Describe("IstioRevision resource", Label("istiorevision"), Ordered, func Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "IstioRevision", Name: revName, }, diff --git a/tests/integration/api/istiorevisiontag_test.go b/tests/integration/api/istiorevisiontag_test.go index 5dc421655..9f6eb5b3b 100644 --- a/tests/integration/api/istiorevisiontag_test.go +++ b/tests/integration/api/istiorevisiontag_test.go @@ -130,7 +130,7 @@ var _ = Describe("IstioRevisionTag resource", Label("istiorevisiontag"), Ordered When("creating the IstioRevisionTag", func() { BeforeAll(func() { - targetRef := v1.IstioRevisionTagTargetReference{ + targetRef := v1.TargetReference{ Kind: referencedResource, Name: getRevisionName(istio, istio.Spec.Version), } @@ -256,7 +256,7 @@ var _ = Describe("IstioRevisionTag resource", Label("istiorevisiontag"), Ordered Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "Istio", Name: istioName, }, @@ -301,7 +301,7 @@ var _ = Describe("IstioRevisionTag resource", Label("istiorevisiontag"), Ordered Name: "default", }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "Istio", Name: istioName, }, @@ -387,7 +387,7 @@ var _ = Describe("IstioRevisionTag resource", Label("istiorevisiontag"), Ordered Name: defaultTagName, }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "Istio", Name: istioName, }, @@ -434,7 +434,7 @@ var _ = Describe("IstioRevisionTag resource", Label("istiorevisiontag"), Ordered Name: defaultTagName, }, Spec: v1.IstioRevisionTagSpec{ - TargetRef: v1.IstioRevisionTagTargetReference{ + TargetRef: v1.TargetReference{ Kind: "Istio", Name: istioName, }, diff --git a/tests/integration/api/ztunnel_test.go b/tests/integration/api/ztunnel_test.go index bfb2840d6..9a9b1b997 100644 --- a/tests/integration/api/ztunnel_test.go +++ b/tests/integration/api/ztunnel_test.go @@ -33,6 +33,8 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + "istio.io/istio/pkg/ptr" ) const ( @@ -231,6 +233,240 @@ var _ = Describe("ZTunnel FIPS", Label("ztunnel", "fips"), Ordered, func() { }) }) +var _ = Describe("ZTunnel targetRef", Label("ztunnel", "targetRef"), Ordered, func() { + SetDefaultEventuallyPollingInterval(time.Second) + SetDefaultEventuallyTimeout(30 * time.Second) + + ctx := context.Background() + + const ( + targetRefIstioName = "target-ref-istio" + targetRefIstioNamespace = "ztunnel-targetref-test" + customHub = "custom-registry.example.com/istio" + ) + + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetRefIstioNamespace, + }, + } + + daemonsetKey := client.ObjectKey{Name: "ztunnel", Namespace: targetRefIstioNamespace} + + var istio *v1.Istio + + BeforeAll(func() { + Expect(k8sClient.Create(ctx, namespace)).To(Succeed()) + + istio = &v1.Istio{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetRefIstioName, + }, + Spec: v1.IstioSpec{ + Version: istioversion.Default, + Namespace: targetRefIstioNamespace, + UpdateStrategy: &v1.IstioUpdateStrategy{ + Type: v1.UpdateStrategyTypeInPlace, + }, + Values: &v1.Values{ + Pilot: &v1.PilotConfig{ + Image: ptr.Of("sail-operator/test:latest"), + Cni: &v1.CNIUsageConfig{ + Enabled: ptr.Of(true), + }, + }, + Global: &v1.GlobalConfig{ + Hub: ptr.Of(customHub), + LogAsJson: ptr.Of(true), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, istio)).To(Succeed()) + + // Wait for Istio to have an active revision + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: targetRefIstioName}, istio)).To(Succeed()) + g.Expect(istio.Status.ActiveRevisionName).ToNot(BeEmpty()) + }).Should(Succeed()) + }) + + AfterAll(func() { + deleteAllIstiosAndRevisions(ctx) + Expect(k8sClient.Delete(ctx, namespace)).To(Succeed()) + }) + + When("creating a ZTunnel with targetRef referencing an Istio resource", func() { + BeforeAll(func() { + ztunnel := &v1.ZTunnel{ + ObjectMeta: metav1.ObjectMeta{ + Name: ztunnelName, + }, + Spec: v1.ZTunnelSpec{ + Version: istioversion.Default, + Namespace: targetRefIstioNamespace, + TargetRef: &v1.TargetReference{ + Kind: v1.IstioKind, + Name: targetRefIstioName, + }, + }, + } + Expect(k8sClient.Create(ctx, ztunnel)).To(Succeed()) + }) + + AfterAll(func() { + ztunnel := &v1.ZTunnel{} + Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + Expect(k8sClient.Delete(ctx, ztunnel)).To(Succeed()) + Eventually(k8sClient.Get).WithArguments(ctx, ztunnelKey, &v1.ZTunnel{}).Should(ReturnNotFoundError()) + }) + + It("creates the ztunnel DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(k8sClient.Get).WithArguments(ctx, daemonsetKey, ds).Should(Succeed()) + }) + + It("sets the IstioRevision in the ZTunnel status", func() { + Eventually(func(g Gomega) { + ztunnel := &v1.ZTunnel{} + g.Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + g.Expect(ztunnel.Status.ObservedGeneration).To(Equal(ztunnel.Generation)) + g.Expect(ztunnel.Status.IstioRevision).To(Equal(istio.Status.ActiveRevisionName)) + }).Should(Succeed()) + }) + + It("is reconciled successfully", func() { + expectZTunnelV1Condition(ctx, v1.ZTunnelConditionReconciled, metav1.ConditionTrue) + }) + + It("copies global.hub from the referenced IstioRevision to the DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, daemonsetKey, ds)).To(Succeed()) + g.Expect(ds.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + g.Expect(ds.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring(customHub)) + }).Should(Succeed()) + }) + + It("copies global.logAsJson from the referenced IstioRevision to the DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, daemonsetKey, ds)).To(Succeed()) + g.Expect(ds.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + g.Expect(ds.Spec.Template.Spec.Containers[0].Env).To( + ContainElement(corev1.EnvVar{Name: "LOG_FORMAT", Value: "json"})) + }).Should(Succeed()) + }) + }) + + When("creating a ZTunnel with targetRef referencing an IstioRevision resource", func() { + var revisionName string + + BeforeAll(func() { + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: targetRefIstioName}, istio)).To(Succeed()) + revisionName = istio.Status.ActiveRevisionName + Expect(revisionName).ToNot(BeEmpty()) + + ztunnel := &v1.ZTunnel{ + ObjectMeta: metav1.ObjectMeta{ + Name: ztunnelName, + }, + Spec: v1.ZTunnelSpec{ + Version: istioversion.Default, + Namespace: targetRefIstioNamespace, + TargetRef: &v1.TargetReference{ + Kind: v1.IstioRevisionKind, + Name: revisionName, + }, + }, + } + Expect(k8sClient.Create(ctx, ztunnel)).To(Succeed()) + }) + + AfterAll(func() { + ztunnel := &v1.ZTunnel{} + Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + Expect(k8sClient.Delete(ctx, ztunnel)).To(Succeed()) + Eventually(k8sClient.Get).WithArguments(ctx, ztunnelKey, &v1.ZTunnel{}).Should(ReturnNotFoundError()) + }) + + It("creates the ztunnel DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(k8sClient.Get).WithArguments(ctx, daemonsetKey, ds).Should(Succeed()) + }) + + It("sets the IstioRevision in the ZTunnel status", func() { + Eventually(func(g Gomega) { + ztunnel := &v1.ZTunnel{} + g.Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + g.Expect(ztunnel.Status.ObservedGeneration).To(Equal(ztunnel.Generation)) + g.Expect(ztunnel.Status.IstioRevision).To(Equal(revisionName)) + }).Should(Succeed()) + }) + + It("is reconciled successfully", func() { + expectZTunnelV1Condition(ctx, v1.ZTunnelConditionReconciled, metav1.ConditionTrue) + }) + + It("copies global.hub from the referenced IstioRevision to the DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, daemonsetKey, ds)).To(Succeed()) + g.Expect(ds.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + g.Expect(ds.Spec.Template.Spec.Containers[0].Image).To(ContainSubstring(customHub)) + }).Should(Succeed()) + }) + + It("copies global.logAsJson from the referenced IstioRevision to the DaemonSet", func() { + ds := &appsv1.DaemonSet{} + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, daemonsetKey, ds)).To(Succeed()) + g.Expect(ds.Spec.Template.Spec.Containers).ToNot(BeEmpty()) + g.Expect(ds.Spec.Template.Spec.Containers[0].Env).To( + ContainElement(corev1.EnvVar{Name: "LOG_FORMAT", Value: "json"})) + }).Should(Succeed()) + }) + }) + + When("creating a ZTunnel with targetRef referencing a non-existent Istio", func() { + BeforeAll(func() { + ztunnel := &v1.ZTunnel{ + ObjectMeta: metav1.ObjectMeta{ + Name: ztunnelName, + }, + Spec: v1.ZTunnelSpec{ + Version: istioversion.Default, + Namespace: targetRefIstioNamespace, + TargetRef: &v1.TargetReference{ + Kind: v1.IstioKind, + Name: "non-existent-istio", + }, + }, + } + Expect(k8sClient.Create(ctx, ztunnel)).To(Succeed()) + }) + + AfterAll(func() { + ztunnel := &v1.ZTunnel{} + Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + Expect(k8sClient.Delete(ctx, ztunnel)).To(Succeed()) + Eventually(k8sClient.Get).WithArguments(ctx, ztunnelKey, &v1.ZTunnel{}).Should(ReturnNotFoundError()) + }) + + It("fails reconciliation", func() { + expectZTunnelV1Condition(ctx, v1.ZTunnelConditionReconciled, metav1.ConditionFalse) + }) + + It("does not set IstioRevision in status", func() { + Eventually(func(g Gomega) { + ztunnel := &v1.ZTunnel{} + g.Expect(k8sClient.Get(ctx, ztunnelKey, ztunnel)).To(Succeed()) + g.Expect(ztunnel.Status.IstioRevision).To(BeEmpty()) + }).Should(Succeed()) + }) + }) +}) + func HaveContainersThat(matcher types.GomegaMatcher) types.GomegaMatcher { return HaveField("Spec.Template.Spec.Containers", matcher) } @@ -240,35 +476,21 @@ func getEnvVars(container corev1.Container) []corev1.EnvVar { } // expectZTunnelV1Condition on the v1.ZTunnel resource to eventually have a given status. -func expectZTunnelV1Condition(ctx context.Context, condition v1.ZTunnelConditionType, status metav1.ConditionStatus, - extraChecks ...func(Gomega, *v1.ZTunnelCondition), -) { +func expectZTunnelV1Condition(ctx context.Context, conditionType v1.ZTunnelConditionType, status metav1.ConditionStatus) { ztunnel := v1.ZTunnel{} Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, ztunnelKey, &ztunnel)).To(Succeed()) g.Expect(ztunnel.Status.ObservedGeneration).To(Equal(ztunnel.ObjectMeta.Generation)) - - condition := ztunnel.Status.GetCondition(condition) - g.Expect(condition.Status).To(Equal(status)) - for _, check := range extraChecks { - check(g, &condition) - } + g.Expect(ztunnel.Status.GetCondition(conditionType).Status).To(Equal(status)) }).Should(Succeed()) } // expectZTunnelV1Alpha1Condition on the v1alpha1.ZTunnel resource to eventually have a given status. -func expectZTunnelV1Alpha1Condition(ctx context.Context, condition v1alpha1.ZTunnelConditionType, status metav1.ConditionStatus, - extraChecks ...func(Gomega, *v1alpha1.ZTunnelCondition), -) { +func expectZTunnelV1Alpha1Condition(ctx context.Context, conditionType v1alpha1.ZTunnelConditionType, status metav1.ConditionStatus) { ztunnel := v1alpha1.ZTunnel{} Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, ztunnelKey, &ztunnel)).To(Succeed()) g.Expect(ztunnel.Status.ObservedGeneration).To(Equal(ztunnel.ObjectMeta.Generation)) - - condition := ztunnel.Status.GetCondition(condition) - g.Expect(condition.Status).To(Equal(status)) - for _, check := range extraChecks { - check(g, &condition) - } + g.Expect(ztunnel.Status.GetCondition(conditionType).Status).To(Equal(status)) }).Should(Succeed()) }