mirror of https://github.com/k3s-io/k3s
511 lines
17 KiB
Go
511 lines
17 KiB
Go
/*
|
|
Copyright 2017 The Kubernetes 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 fuzzer
|
|
|
|
import (
|
|
"reflect"
|
|
"strconv"
|
|
"time"
|
|
|
|
fuzz "github.com/google/gofuzz"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/kubernetes/pkg/apis/core"
|
|
)
|
|
|
|
// Funcs returns the fuzzer functions for the core group.
|
|
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
|
|
return []interface{}{
|
|
func(q *resource.Quantity, c fuzz.Continue) {
|
|
*q = *resource.NewQuantity(c.Int63n(1000), resource.DecimalExponent)
|
|
},
|
|
func(j *core.ObjectReference, c fuzz.Continue) {
|
|
// We have to customize the randomization of TypeMetas because their
|
|
// APIVersion and Kind must remain blank in memory.
|
|
j.APIVersion = c.RandString()
|
|
j.Kind = c.RandString()
|
|
j.Namespace = c.RandString()
|
|
j.Name = c.RandString()
|
|
j.ResourceVersion = strconv.FormatUint(c.RandUint64(), 10)
|
|
j.FieldPath = c.RandString()
|
|
},
|
|
func(j *core.PodExecOptions, c fuzz.Continue) {
|
|
j.Stdout = true
|
|
j.Stderr = true
|
|
},
|
|
func(j *core.PodAttachOptions, c fuzz.Continue) {
|
|
j.Stdout = true
|
|
j.Stderr = true
|
|
},
|
|
func(j *core.PodPortForwardOptions, c fuzz.Continue) {
|
|
if c.RandBool() {
|
|
j.Ports = make([]int32, c.Intn(10))
|
|
for i := range j.Ports {
|
|
j.Ports[i] = c.Int31n(65535)
|
|
}
|
|
}
|
|
},
|
|
func(s *core.PodSpec, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s)
|
|
// has a default value
|
|
ttl := int64(30)
|
|
if c.RandBool() {
|
|
ttl = int64(c.Uint32())
|
|
}
|
|
s.TerminationGracePeriodSeconds = &ttl
|
|
|
|
c.Fuzz(s.SecurityContext)
|
|
|
|
if s.SecurityContext == nil {
|
|
s.SecurityContext = new(core.PodSecurityContext)
|
|
}
|
|
if s.Affinity == nil {
|
|
s.Affinity = new(core.Affinity)
|
|
}
|
|
if s.SchedulerName == "" {
|
|
s.SchedulerName = core.DefaultSchedulerName
|
|
}
|
|
if s.EnableServiceLinks == nil {
|
|
enableServiceLinks := corev1.DefaultEnableServiceLinks
|
|
s.EnableServiceLinks = &enableServiceLinks
|
|
}
|
|
},
|
|
func(j *core.PodPhase, c fuzz.Continue) {
|
|
statuses := []core.PodPhase{core.PodPending, core.PodRunning, core.PodFailed, core.PodUnknown}
|
|
*j = statuses[c.Rand.Intn(len(statuses))]
|
|
},
|
|
func(j *core.Binding, c fuzz.Continue) {
|
|
c.Fuzz(&j.ObjectMeta)
|
|
j.Target.Name = c.RandString()
|
|
},
|
|
func(j *core.ReplicationController, c fuzz.Continue) {
|
|
c.FuzzNoCustom(j)
|
|
|
|
// match defaulting
|
|
if j.Spec.Template != nil {
|
|
if len(j.Labels) == 0 {
|
|
j.Labels = j.Spec.Template.Labels
|
|
}
|
|
if len(j.Spec.Selector) == 0 {
|
|
j.Spec.Selector = j.Spec.Template.Labels
|
|
}
|
|
}
|
|
},
|
|
func(j *core.ReplicationControllerSpec, c fuzz.Continue) {
|
|
c.FuzzNoCustom(j) // fuzz self without calling this function again
|
|
//j.TemplateRef = nil // this is required for round trip
|
|
},
|
|
func(j *core.List, c fuzz.Continue) {
|
|
c.FuzzNoCustom(j) // fuzz self without calling this function again
|
|
// TODO: uncomment when round trip starts from a versioned object
|
|
if false { //j.Items == nil {
|
|
j.Items = []runtime.Object{}
|
|
}
|
|
},
|
|
func(q *core.ResourceRequirements, c fuzz.Continue) {
|
|
randomQuantity := func() resource.Quantity {
|
|
var q resource.Quantity
|
|
c.Fuzz(&q)
|
|
// precalc the string for benchmarking purposes
|
|
_ = q.String()
|
|
return q
|
|
}
|
|
q.Limits = make(core.ResourceList)
|
|
q.Requests = make(core.ResourceList)
|
|
cpuLimit := randomQuantity()
|
|
q.Limits[core.ResourceCPU] = *cpuLimit.Copy()
|
|
q.Requests[core.ResourceCPU] = *cpuLimit.Copy()
|
|
memoryLimit := randomQuantity()
|
|
q.Limits[core.ResourceMemory] = *memoryLimit.Copy()
|
|
q.Requests[core.ResourceMemory] = *memoryLimit.Copy()
|
|
storageLimit := randomQuantity()
|
|
q.Limits[core.ResourceStorage] = *storageLimit.Copy()
|
|
q.Requests[core.ResourceStorage] = *storageLimit.Copy()
|
|
},
|
|
func(q *core.LimitRangeItem, c fuzz.Continue) {
|
|
var cpuLimit resource.Quantity
|
|
c.Fuzz(&cpuLimit)
|
|
|
|
q.Type = core.LimitTypeContainer
|
|
q.Default = make(core.ResourceList)
|
|
q.Default[core.ResourceCPU] = *(cpuLimit.Copy())
|
|
|
|
q.DefaultRequest = make(core.ResourceList)
|
|
q.DefaultRequest[core.ResourceCPU] = *(cpuLimit.Copy())
|
|
|
|
q.Max = make(core.ResourceList)
|
|
q.Max[core.ResourceCPU] = *(cpuLimit.Copy())
|
|
|
|
q.Min = make(core.ResourceList)
|
|
q.Min[core.ResourceCPU] = *(cpuLimit.Copy())
|
|
|
|
q.MaxLimitRequestRatio = make(core.ResourceList)
|
|
q.MaxLimitRequestRatio[core.ResourceCPU] = resource.MustParse("10")
|
|
},
|
|
func(p *core.PullPolicy, c fuzz.Continue) {
|
|
policies := []core.PullPolicy{core.PullAlways, core.PullNever, core.PullIfNotPresent}
|
|
*p = policies[c.Rand.Intn(len(policies))]
|
|
},
|
|
func(rp *core.RestartPolicy, c fuzz.Continue) {
|
|
policies := []core.RestartPolicy{core.RestartPolicyAlways, core.RestartPolicyNever, core.RestartPolicyOnFailure}
|
|
*rp = policies[c.Rand.Intn(len(policies))]
|
|
},
|
|
// core.DownwardAPIVolumeFile needs to have a specific func since FieldRef has to be
|
|
// defaulted to a version otherwise roundtrip will fail
|
|
func(m *core.DownwardAPIVolumeFile, c fuzz.Continue) {
|
|
m.Path = c.RandString()
|
|
versions := []string{"v1"}
|
|
m.FieldRef = &core.ObjectFieldSelector{}
|
|
m.FieldRef.APIVersion = versions[c.Rand.Intn(len(versions))]
|
|
m.FieldRef.FieldPath = c.RandString()
|
|
c.Fuzz(m.Mode)
|
|
if m.Mode != nil {
|
|
*m.Mode &= 0777
|
|
}
|
|
},
|
|
func(s *core.SecretVolumeSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s) // fuzz self without calling this function again
|
|
|
|
if c.RandBool() {
|
|
opt := c.RandBool()
|
|
s.Optional = &opt
|
|
}
|
|
// DefaultMode should always be set, it has a default
|
|
// value and it is expected to be between 0 and 0777
|
|
var mode int32
|
|
c.Fuzz(&mode)
|
|
mode &= 0777
|
|
s.DefaultMode = &mode
|
|
},
|
|
func(cm *core.ConfigMapVolumeSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(cm) // fuzz self without calling this function again
|
|
|
|
if c.RandBool() {
|
|
opt := c.RandBool()
|
|
cm.Optional = &opt
|
|
}
|
|
// DefaultMode should always be set, it has a default
|
|
// value and it is expected to be between 0 and 0777
|
|
var mode int32
|
|
c.Fuzz(&mode)
|
|
mode &= 0777
|
|
cm.DefaultMode = &mode
|
|
},
|
|
func(d *core.DownwardAPIVolumeSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(d) // fuzz self without calling this function again
|
|
|
|
// DefaultMode should always be set, it has a default
|
|
// value and it is expected to be between 0 and 0777
|
|
var mode int32
|
|
c.Fuzz(&mode)
|
|
mode &= 0777
|
|
d.DefaultMode = &mode
|
|
},
|
|
func(s *core.ProjectedVolumeSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s) // fuzz self without calling this function again
|
|
|
|
// DefaultMode should always be set, it has a default
|
|
// value and it is expected to be between 0 and 0777
|
|
var mode int32
|
|
c.Fuzz(&mode)
|
|
mode &= 0777
|
|
s.DefaultMode = &mode
|
|
},
|
|
func(k *core.KeyToPath, c fuzz.Continue) {
|
|
c.FuzzNoCustom(k) // fuzz self without calling this function again
|
|
k.Key = c.RandString()
|
|
k.Path = c.RandString()
|
|
|
|
// Mode is not mandatory, but if it is set, it should be
|
|
// a value between 0 and 0777
|
|
if k.Mode != nil {
|
|
*k.Mode &= 0777
|
|
}
|
|
},
|
|
func(vs *core.VolumeSource, c fuzz.Continue) {
|
|
// Exactly one of the fields must be set.
|
|
v := reflect.ValueOf(vs).Elem()
|
|
i := int(c.RandUint64() % uint64(v.NumField()))
|
|
t := v.Field(i).Addr()
|
|
for v.Field(i).IsNil() {
|
|
c.Fuzz(t.Interface())
|
|
}
|
|
},
|
|
func(i *core.ISCSIVolumeSource, c fuzz.Continue) {
|
|
i.ISCSIInterface = c.RandString()
|
|
if i.ISCSIInterface == "" {
|
|
i.ISCSIInterface = "default"
|
|
}
|
|
},
|
|
func(i *core.ISCSIPersistentVolumeSource, c fuzz.Continue) {
|
|
i.ISCSIInterface = c.RandString()
|
|
if i.ISCSIInterface == "" {
|
|
i.ISCSIInterface = "default"
|
|
}
|
|
},
|
|
func(d *core.DNSPolicy, c fuzz.Continue) {
|
|
policies := []core.DNSPolicy{core.DNSClusterFirst, core.DNSDefault}
|
|
*d = policies[c.Rand.Intn(len(policies))]
|
|
},
|
|
func(p *core.Protocol, c fuzz.Continue) {
|
|
protocols := []core.Protocol{core.ProtocolTCP, core.ProtocolUDP, core.ProtocolSCTP}
|
|
*p = protocols[c.Rand.Intn(len(protocols))]
|
|
},
|
|
func(p *core.ServiceAffinity, c fuzz.Continue) {
|
|
types := []core.ServiceAffinity{core.ServiceAffinityClientIP, core.ServiceAffinityNone}
|
|
*p = types[c.Rand.Intn(len(types))]
|
|
},
|
|
func(p *core.ServiceType, c fuzz.Continue) {
|
|
types := []core.ServiceType{core.ServiceTypeClusterIP, core.ServiceTypeNodePort, core.ServiceTypeLoadBalancer}
|
|
*p = types[c.Rand.Intn(len(types))]
|
|
},
|
|
func(p *core.ServiceExternalTrafficPolicyType, c fuzz.Continue) {
|
|
types := []core.ServiceExternalTrafficPolicyType{core.ServiceExternalTrafficPolicyTypeCluster, core.ServiceExternalTrafficPolicyTypeLocal}
|
|
*p = types[c.Rand.Intn(len(types))]
|
|
},
|
|
func(ct *core.Container, c fuzz.Continue) {
|
|
c.FuzzNoCustom(ct) // fuzz self without calling this function again
|
|
ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty
|
|
ct.TerminationMessagePolicy = "File"
|
|
},
|
|
func(p *core.Probe, c fuzz.Continue) {
|
|
c.FuzzNoCustom(p)
|
|
// These fields have default values.
|
|
intFieldsWithDefaults := [...]string{"TimeoutSeconds", "PeriodSeconds", "SuccessThreshold", "FailureThreshold"}
|
|
v := reflect.ValueOf(p).Elem()
|
|
for _, field := range intFieldsWithDefaults {
|
|
f := v.FieldByName(field)
|
|
if f.Int() == 0 {
|
|
f.SetInt(1)
|
|
}
|
|
}
|
|
},
|
|
func(ev *core.EnvVar, c fuzz.Continue) {
|
|
ev.Name = c.RandString()
|
|
if c.RandBool() {
|
|
ev.Value = c.RandString()
|
|
} else {
|
|
ev.ValueFrom = &core.EnvVarSource{}
|
|
ev.ValueFrom.FieldRef = &core.ObjectFieldSelector{}
|
|
|
|
versions := []schema.GroupVersion{
|
|
{Group: "admission.k8s.io", Version: "v1alpha1"},
|
|
{Group: "apps", Version: "v1beta1"},
|
|
{Group: "apps", Version: "v1beta2"},
|
|
{Group: "foo", Version: "v42"},
|
|
}
|
|
|
|
ev.ValueFrom.FieldRef.APIVersion = versions[c.Rand.Intn(len(versions))].String()
|
|
ev.ValueFrom.FieldRef.FieldPath = c.RandString()
|
|
}
|
|
},
|
|
func(ev *core.EnvFromSource, c fuzz.Continue) {
|
|
if c.RandBool() {
|
|
ev.Prefix = "p_"
|
|
}
|
|
if c.RandBool() {
|
|
c.Fuzz(&ev.ConfigMapRef)
|
|
} else {
|
|
c.Fuzz(&ev.SecretRef)
|
|
}
|
|
},
|
|
func(cm *core.ConfigMapEnvSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(cm) // fuzz self without calling this function again
|
|
if c.RandBool() {
|
|
opt := c.RandBool()
|
|
cm.Optional = &opt
|
|
}
|
|
},
|
|
func(s *core.SecretEnvSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s) // fuzz self without calling this function again
|
|
},
|
|
func(sc *core.SecurityContext, c fuzz.Continue) {
|
|
c.FuzzNoCustom(sc) // fuzz self without calling this function again
|
|
if c.RandBool() {
|
|
priv := c.RandBool()
|
|
sc.Privileged = &priv
|
|
}
|
|
|
|
if c.RandBool() {
|
|
sc.Capabilities = &core.Capabilities{
|
|
Add: make([]core.Capability, 0),
|
|
Drop: make([]core.Capability, 0),
|
|
}
|
|
c.Fuzz(&sc.Capabilities.Add)
|
|
c.Fuzz(&sc.Capabilities.Drop)
|
|
}
|
|
if sc.ProcMount == nil {
|
|
defProcMount := core.DefaultProcMount
|
|
sc.ProcMount = &defProcMount
|
|
}
|
|
},
|
|
func(s *core.Secret, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s) // fuzz self without calling this function again
|
|
s.Type = core.SecretTypeOpaque
|
|
},
|
|
func(r *core.RBDVolumeSource, c fuzz.Continue) {
|
|
r.RBDPool = c.RandString()
|
|
if r.RBDPool == "" {
|
|
r.RBDPool = "rbd"
|
|
}
|
|
r.RadosUser = c.RandString()
|
|
if r.RadosUser == "" {
|
|
r.RadosUser = "admin"
|
|
}
|
|
r.Keyring = c.RandString()
|
|
if r.Keyring == "" {
|
|
r.Keyring = "/etc/ceph/keyring"
|
|
}
|
|
},
|
|
func(r *core.RBDPersistentVolumeSource, c fuzz.Continue) {
|
|
r.RBDPool = c.RandString()
|
|
if r.RBDPool == "" {
|
|
r.RBDPool = "rbd"
|
|
}
|
|
r.RadosUser = c.RandString()
|
|
if r.RadosUser == "" {
|
|
r.RadosUser = "admin"
|
|
}
|
|
r.Keyring = c.RandString()
|
|
if r.Keyring == "" {
|
|
r.Keyring = "/etc/ceph/keyring"
|
|
}
|
|
},
|
|
func(obj *core.HostPathVolumeSource, c fuzz.Continue) {
|
|
c.FuzzNoCustom(obj)
|
|
types := []core.HostPathType{core.HostPathUnset, core.HostPathDirectoryOrCreate, core.HostPathDirectory,
|
|
core.HostPathFileOrCreate, core.HostPathFile, core.HostPathSocket, core.HostPathCharDev, core.HostPathBlockDev}
|
|
typeVol := types[c.Rand.Intn(len(types))]
|
|
if obj.Type == nil {
|
|
obj.Type = &typeVol
|
|
}
|
|
},
|
|
func(pv *core.PersistentVolume, c fuzz.Continue) {
|
|
c.FuzzNoCustom(pv) // fuzz self without calling this function again
|
|
types := []core.PersistentVolumePhase{core.VolumeAvailable, core.VolumePending, core.VolumeBound, core.VolumeReleased, core.VolumeFailed}
|
|
pv.Status.Phase = types[c.Rand.Intn(len(types))]
|
|
pv.Status.Message = c.RandString()
|
|
reclamationPolicies := []core.PersistentVolumeReclaimPolicy{core.PersistentVolumeReclaimRecycle, core.PersistentVolumeReclaimRetain}
|
|
pv.Spec.PersistentVolumeReclaimPolicy = reclamationPolicies[c.Rand.Intn(len(reclamationPolicies))]
|
|
volumeModes := []core.PersistentVolumeMode{core.PersistentVolumeFilesystem, core.PersistentVolumeBlock}
|
|
pv.Spec.VolumeMode = &volumeModes[c.Rand.Intn(len(volumeModes))]
|
|
},
|
|
func(pvc *core.PersistentVolumeClaim, c fuzz.Continue) {
|
|
c.FuzzNoCustom(pvc) // fuzz self without calling this function again
|
|
types := []core.PersistentVolumeClaimPhase{core.ClaimBound, core.ClaimPending, core.ClaimLost}
|
|
pvc.Status.Phase = types[c.Rand.Intn(len(types))]
|
|
volumeModes := []core.PersistentVolumeMode{core.PersistentVolumeFilesystem, core.PersistentVolumeBlock}
|
|
pvc.Spec.VolumeMode = &volumeModes[c.Rand.Intn(len(volumeModes))]
|
|
},
|
|
func(obj *core.AzureDiskVolumeSource, c fuzz.Continue) {
|
|
if obj.CachingMode == nil {
|
|
obj.CachingMode = new(core.AzureDataDiskCachingMode)
|
|
*obj.CachingMode = core.AzureDataDiskCachingReadWrite
|
|
}
|
|
if obj.Kind == nil {
|
|
obj.Kind = new(core.AzureDataDiskKind)
|
|
*obj.Kind = core.AzureSharedBlobDisk
|
|
}
|
|
if obj.FSType == nil {
|
|
obj.FSType = new(string)
|
|
*obj.FSType = "ext4"
|
|
}
|
|
if obj.ReadOnly == nil {
|
|
obj.ReadOnly = new(bool)
|
|
*obj.ReadOnly = false
|
|
}
|
|
},
|
|
func(sio *core.ScaleIOVolumeSource, c fuzz.Continue) {
|
|
sio.StorageMode = c.RandString()
|
|
if sio.StorageMode == "" {
|
|
sio.StorageMode = "ThinProvisioned"
|
|
}
|
|
sio.FSType = c.RandString()
|
|
if sio.FSType == "" {
|
|
sio.FSType = "xfs"
|
|
}
|
|
},
|
|
func(sio *core.ScaleIOPersistentVolumeSource, c fuzz.Continue) {
|
|
sio.StorageMode = c.RandString()
|
|
if sio.StorageMode == "" {
|
|
sio.StorageMode = "ThinProvisioned"
|
|
}
|
|
sio.FSType = c.RandString()
|
|
if sio.FSType == "" {
|
|
sio.FSType = "xfs"
|
|
}
|
|
},
|
|
func(s *core.NamespaceSpec, c fuzz.Continue) {
|
|
s.Finalizers = []core.FinalizerName{core.FinalizerKubernetes}
|
|
},
|
|
func(s *core.NamespaceStatus, c fuzz.Continue) {
|
|
s.Phase = core.NamespaceActive
|
|
},
|
|
func(http *core.HTTPGetAction, c fuzz.Continue) {
|
|
c.FuzzNoCustom(http) // fuzz self without calling this function again
|
|
http.Path = "/" + http.Path // can't be blank
|
|
http.Scheme = "x" + http.Scheme // can't be blank
|
|
},
|
|
func(ss *core.ServiceSpec, c fuzz.Continue) {
|
|
c.FuzzNoCustom(ss) // fuzz self without calling this function again
|
|
if len(ss.Ports) == 0 {
|
|
// There must be at least 1 port.
|
|
ss.Ports = append(ss.Ports, core.ServicePort{})
|
|
c.Fuzz(&ss.Ports[0])
|
|
}
|
|
for i := range ss.Ports {
|
|
switch ss.Ports[i].TargetPort.Type {
|
|
case intstr.Int:
|
|
ss.Ports[i].TargetPort.IntVal = 1 + ss.Ports[i].TargetPort.IntVal%65535 // non-zero
|
|
case intstr.String:
|
|
ss.Ports[i].TargetPort.StrVal = "x" + ss.Ports[i].TargetPort.StrVal // non-empty
|
|
}
|
|
}
|
|
types := []core.ServiceAffinity{core.ServiceAffinityNone, core.ServiceAffinityClientIP}
|
|
ss.SessionAffinity = types[c.Rand.Intn(len(types))]
|
|
switch ss.SessionAffinity {
|
|
case core.ServiceAffinityClientIP:
|
|
timeoutSeconds := int32(c.Rand.Intn(int(core.MaxClientIPServiceAffinitySeconds)))
|
|
ss.SessionAffinityConfig = &core.SessionAffinityConfig{
|
|
ClientIP: &core.ClientIPConfig{
|
|
TimeoutSeconds: &timeoutSeconds,
|
|
},
|
|
}
|
|
case core.ServiceAffinityNone:
|
|
ss.SessionAffinityConfig = nil
|
|
}
|
|
},
|
|
func(s *core.NodeStatus, c fuzz.Continue) {
|
|
c.FuzzNoCustom(s)
|
|
s.Allocatable = s.Capacity
|
|
},
|
|
func(e *core.Event, c fuzz.Continue) {
|
|
c.FuzzNoCustom(e)
|
|
e.EventTime = metav1.MicroTime{Time: time.Unix(1, 1000)}
|
|
if e.Series != nil {
|
|
e.Series.LastObservedTime = metav1.MicroTime{Time: time.Unix(3, 3000)}
|
|
}
|
|
},
|
|
}
|
|
}
|