diff --git a/apis/bases/core.openstack.org_openstackcontrolplanes.yaml b/apis/bases/core.openstack.org_openstackcontrolplanes.yaml index 9f682503d..b0862fa91 100644 --- a/apis/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/apis/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -8284,6 +8284,107 @@ spec: type: object octavia: properties: + apiOverride: + properties: + route: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + alternateBackends: + items: + properties: + kind: + enum: + - Service + - "" + type: string + name: + type: string + weight: + format: int32 + maximum: 256 + minimum: 0 + type: integer + type: object + maxItems: 3 + type: array + host: + maxLength: 253 + pattern: ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$ + type: string + path: + pattern: ^/ + type: string + port: + properties: + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - targetPort + type: object + subdomain: + maxLength: 253 + pattern: ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$ + type: string + tls: + properties: + caCertificate: + type: string + certificate: + type: string + destinationCACertificate: + type: string + insecureEdgeTerminationPolicy: + type: string + key: + type: string + termination: + enum: + - edge + - reencrypt + - passthrough + type: string + required: + - termination + type: object + to: + properties: + kind: + enum: + - Service + - "" + type: string + name: + type: string + weight: + format: int32 + maximum: 256 + minimum: 0 + type: integer + type: object + wildcardPolicy: + enum: + - None + - Subdomain + - "" + type: string + type: object + type: object + type: object enabled: default: false type: boolean @@ -8345,6 +8446,57 @@ spec: additionalProperties: type: string type: object + override: + properties: + service: + additionalProperties: + properties: + endpointURL: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + externalName: + type: string + externalTrafficPolicy: + type: string + internalTrafficPolicy: + type: string + ipFamilyPolicy: + type: string + loadBalancerClass: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + sessionAffinity: + type: string + sessionAffinityConfig: + properties: + clientIP: + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + type: + type: string + type: object + type: object + type: object + type: object passwordSelectors: default: database: OctaviaDatabasePassword @@ -8410,6 +8562,279 @@ spec: - secret - serviceAccount type: object + octaviaHealthManager: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object + octaviaHousekeeping: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object + octaviaWorker: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object passwordSelectors: default: database: OctaviaDatabasePassword @@ -8425,6 +8850,9 @@ spec: preserveJobs: default: false type: boolean + rabbitMqClusterName: + default: rabbitmq + type: string secret: type: string serviceUser: @@ -8433,6 +8861,7 @@ spec: required: - databaseInstance - octaviaAPI + - rabbitMqClusterName - secret type: object type: object diff --git a/apis/core/v1beta1/conditions.go b/apis/core/v1beta1/conditions.go index 551f3b8f8..900b30888 100644 --- a/apis/core/v1beta1/conditions.go +++ b/apis/core/v1beta1/conditions.go @@ -143,6 +143,9 @@ const ( // OpenStackControlPlaneRedisReadyCondition Status=True condition which indicates if Redis is configured and operational OpenStackControlPlaneRedisReadyCondition condition.Type = "OpenStackControlPlaneRedisReady" + + // OpenStackControlPlaneExposeOctaviaReadyCondition Status=True condition which indicates if Octavia is exposed via a route + OpenStackControlPlaneExposeOctaviaReadyCondition condition.Type = "OpenStackControlPlaneExposeOctaviaReady" ) // OpenStackControlPlane Reasons used by API objects. diff --git a/apis/core/v1beta1/openstackcontrolplane_types.go b/apis/core/v1beta1/openstackcontrolplane_types.go index cae50b2d2..abd77c920 100644 --- a/apis/core/v1beta1/openstackcontrolplane_types.go +++ b/apis/core/v1beta1/openstackcontrolplane_types.go @@ -540,6 +540,11 @@ type OctaviaSection struct { // +operator-sdk:csv:customresourcedefinitions:type=spec // Template - Overrides to use when creating Octavia Resources Template octaviav1.OctaviaSpec `json:"template,omitempty"` + + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // APIOverride, provides the ability to override the generated manifest of several child resources. + APIOverride Override `json:"apiOverride,omitempty"` } // RedisSection defines the desired state of the Redis service diff --git a/apis/core/v1beta1/zz_generated.deepcopy.go b/apis/core/v1beta1/zz_generated.deepcopy.go index 6b5506778..bbe5b7cdc 100644 --- a/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/apis/core/v1beta1/zz_generated.deepcopy.go @@ -312,6 +312,7 @@ func (in *NovaSection) DeepCopy() *NovaSection { func (in *OctaviaSection) DeepCopyInto(out *OctaviaSection) { *out = *in in.Template.DeepCopyInto(&out.Template) + in.APIOverride.DeepCopyInto(&out.APIOverride) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OctaviaSection. diff --git a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml index 9f682503d..b0862fa91 100644 --- a/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml +++ b/config/crd/bases/core.openstack.org_openstackcontrolplanes.yaml @@ -8284,6 +8284,107 @@ spec: type: object octavia: properties: + apiOverride: + properties: + route: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + alternateBackends: + items: + properties: + kind: + enum: + - Service + - "" + type: string + name: + type: string + weight: + format: int32 + maximum: 256 + minimum: 0 + type: integer + type: object + maxItems: 3 + type: array + host: + maxLength: 253 + pattern: ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$ + type: string + path: + pattern: ^/ + type: string + port: + properties: + targetPort: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + required: + - targetPort + type: object + subdomain: + maxLength: 253 + pattern: ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$ + type: string + tls: + properties: + caCertificate: + type: string + certificate: + type: string + destinationCACertificate: + type: string + insecureEdgeTerminationPolicy: + type: string + key: + type: string + termination: + enum: + - edge + - reencrypt + - passthrough + type: string + required: + - termination + type: object + to: + properties: + kind: + enum: + - Service + - "" + type: string + name: + type: string + weight: + format: int32 + maximum: 256 + minimum: 0 + type: integer + type: object + wildcardPolicy: + enum: + - None + - Subdomain + - "" + type: string + type: object + type: object + type: object enabled: default: false type: boolean @@ -8345,6 +8446,57 @@ spec: additionalProperties: type: string type: object + override: + properties: + service: + additionalProperties: + properties: + endpointURL: + type: string + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + spec: + properties: + externalName: + type: string + externalTrafficPolicy: + type: string + internalTrafficPolicy: + type: string + ipFamilyPolicy: + type: string + loadBalancerClass: + type: string + loadBalancerSourceRanges: + items: + type: string + type: array + sessionAffinity: + type: string + sessionAffinityConfig: + properties: + clientIP: + properties: + timeoutSeconds: + format: int32 + type: integer + type: object + type: object + type: + type: string + type: object + type: object + type: object + type: object passwordSelectors: default: database: OctaviaDatabasePassword @@ -8410,6 +8562,279 @@ spec: - secret - serviceAccount type: object + octaviaHealthManager: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object + octaviaHousekeeping: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object + octaviaWorker: + properties: + certssecret: + type: string + containerImage: + type: string + customServiceConfig: + default: '# add your customization here' + type: string + databaseHostname: + type: string + databaseInstance: + type: string + databaseUser: + default: octavia + type: string + defaultConfigOverwrite: + additionalProperties: + type: string + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passwordSelectors: + default: + database: OctaviaDatabasePassword + service: OctaviaPassword + properties: + database: + default: OctaviaDatabasePassword + type: string + service: + default: OctaviaPassword + type: string + type: object + replicas: + default: 1 + format: int32 + maximum: 32 + minimum: 0 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + role: + type: string + secret: + type: string + serviceAccount: + type: string + serviceUser: + default: octavia + type: string + transportURLSecret: + type: string + required: + - certssecret + - databaseInstance + - role + - secret + - serviceAccount + type: object passwordSelectors: default: database: OctaviaDatabasePassword @@ -8425,6 +8850,9 @@ spec: preserveJobs: default: false type: boolean + rabbitMqClusterName: + default: rabbitmq + type: string secret: type: string serviceUser: @@ -8433,6 +8861,7 @@ spec: required: - databaseInstance - octaviaAPI + - rabbitMqClusterName - secret type: object type: object diff --git a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml index 792b40cc3..f9e92873d 100644 --- a/config/manifests/bases/openstack-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/openstack-operator.clusterserviceversion.yaml @@ -224,6 +224,10 @@ spec: - description: Template - Overrides to use when creating the Nova services displayName: Template path: nova.template + - description: APIOverride, provides the ability to override the generated manifest + of several child resources. + displayName: APIOverride + path: octavia.apiOverride - description: Enabled - Whether the Octavia service should be deployed and managed displayName: Enabled diff --git a/pkg/openstack/octavia.go b/pkg/openstack/octavia.go index 6429a0eee..6715197f1 100644 --- a/pkg/openstack/octavia.go +++ b/pkg/openstack/octavia.go @@ -20,14 +20,19 @@ import ( "context" "fmt" + "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" corev1beta1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" ) @@ -45,9 +50,57 @@ func ReconcileOctavia(ctx context.Context, instance *corev1beta1.OpenStackContro return res, err } instance.Status.Conditions.Remove(corev1beta1.OpenStackControlPlaneOctaviaReadyCondition) + instance.Status.Conditions.Remove(corev1beta1.OpenStackControlPlaneExposeOctaviaReadyCondition) return ctrl.Result{}, nil } + // add selector to service overrides + for _, endpointType := range []service.Endpoint{service.EndpointPublic, service.EndpointInternal} { + if instance.Spec.Octavia.Template.OctaviaAPI.Override.Service == nil { + instance.Spec.Octavia.Template.OctaviaAPI.Override.Service = map[service.Endpoint]service.RoutedOverrideSpec{} + } + instance.Spec.Octavia.Template.OctaviaAPI.Override.Service[endpointType] = + AddServiceComponentLabel( + instance.Spec.Octavia.Template.OctaviaAPI.Override.Service[endpointType], + octavia.Name) + } + + // When component services got created check if there is the need to create a route + if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: "octavia", Namespace: instance.Namespace}, octavia); err != nil { + if !k8s_errors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + + if octavia.Status.Conditions.IsTrue(condition.ReadyCondition) { + svcs, err := service.GetServicesListWithLabel( + ctx, + helper, + instance.Namespace, + map[string]string{common.AppSelector: octavia.Name}, + ) + if err != nil { + return ctrl.Result{}, err + } + + var ctrlResult reconcile.Result + instance.Spec.Octavia.Template.OctaviaAPI.Override.Service, ctrlResult, err = EnsureRoute( + ctx, + instance, + helper, + octavia, + svcs, + instance.Spec.Octavia.Template.OctaviaAPI.Override.Service, + instance.Spec.Octavia.APIOverride.Route, + corev1beta1.OpenStackControlPlaneExposeOctaviaReadyCondition, + ) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + } + helper.GetLogger().Info("Reconciling Octavia", "Octavia.Namespace", instance.Namespace, "Octavia.Name", octavia.Name) op, err := controllerutil.CreateOrPatch(ctx, helper.GetClient(), octavia, func() error { instance.Spec.Octavia.Template.DeepCopyInto(&octavia.Spec)