Skip to content

Commit

Permalink
Merge branch 'tektoncd:main' into feature/s2i-python-base-image-param…
Browse files Browse the repository at this point in the history
…eter
  • Loading branch information
sveno1990 authored Nov 14, 2024
2 parents d9e9247 + d778e3a commit 5406e20
Show file tree
Hide file tree
Showing 35 changed files with 510 additions and 341 deletions.
4 changes: 2 additions & 2 deletions components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pipeline:
version: v0.65.1
pipelines-as-code:
github: openshift-pipelines/pipelines-as-code
version: v0.28.1
version: v0.29.0
results:
github: tektoncd/results
version: v0.12.2
version: v0.13.0
triggers:
github: tektoncd/triggers
version: v0.30.0
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ require (
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/tektoncd/pipeline v0.65.0
github.com/tektoncd/pipeline v0.65.1
github.com/tektoncd/plumbing v0.0.0-20231109154454-9ef46b417293
github.com/tektoncd/triggers v0.29.1
github.com/tektoncd/triggers v0.30.0
go.opencensus.io v0.24.0
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/mod v0.21.0
golang.org/x/sync v0.8.0
golang.org/x/mod v0.22.0
golang.org/x/sync v0.9.0
gomodules.xyz/jsonpatch/v2 v2.4.0
gotest.tools/v3 v3.5.1
k8s.io/api v0.30.0
Expand Down Expand Up @@ -158,11 +158,11 @@ require (
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/cel-go v0.21.0 // indirect
github.com/google/certificate-transparency-go v1.2.1 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-containerregistry v0.20.2 // indirect
Expand Down
23 changes: 12 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1098,8 +1098,9 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
Expand Down Expand Up @@ -1150,8 +1151,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.12.7/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw=
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
github.com/google/certificate-transparency-go v1.2.1 h1:4iW/NwzqOqYEEoCBEFP+jPbBXbLqMpq3CifMyOnDUME=
github.com/google/certificate-transparency-go v1.2.1/go.mod h1:bvn/ytAccv+I6+DGkqpvSsEdiVGramgaSC6RD3tEmeE=
github.com/google/flatbuffers v2.0.8+incompatible h1:ivUb1cGomAB101ZM1T0nOiWz9pSrTMoa9+EiY7igmkM=
Expand Down Expand Up @@ -1761,12 +1762,12 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=
github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/tektoncd/pipeline v0.65.0 h1:MIXXt/OeV/SLQ0KYXXBzPHBwXe2iIhgZcJBHkkgzaYY=
github.com/tektoncd/pipeline v0.65.0/go.mod h1:V3cyfxxc7b3GLT2a13GX2mWA86qmxWhh4mOp4gfFQwQ=
github.com/tektoncd/pipeline v0.65.1 h1:7Ee/nqG+QWE25NGzwKZdFE0p5COb/aljfDysUFv8+0o=
github.com/tektoncd/pipeline v0.65.1/go.mod h1:V3cyfxxc7b3GLT2a13GX2mWA86qmxWhh4mOp4gfFQwQ=
github.com/tektoncd/plumbing v0.0.0-20231109154454-9ef46b417293 h1:kNmGaAtPS9LnfNZG/JrF4Y0Qx5Ju+384aqKJNtk4PU0=
github.com/tektoncd/plumbing v0.0.0-20231109154454-9ef46b417293/go.mod h1:7eWs1XNkmReggow7ggRbRyRuHi7646B8b2XipCZ3VOw=
github.com/tektoncd/triggers v0.29.1 h1:UXqjJICaRsWYb0qkIYOUlqaDR5te9Zmfrz93+TXy3ug=
github.com/tektoncd/triggers v0.29.1/go.mod h1:yVNxCSlYw//uKoXDi4kzzwYGkK2KIYLt6FwwSTz0aj8=
github.com/tektoncd/triggers v0.30.0 h1:1RV3yxRlEN565qHYG8vIKyfrU3QVZkPuv67qurLeSYg=
github.com/tektoncd/triggers v0.30.0/go.mod h1:YkhGaFuL+z4aErBHz66di1dwuDjowmryTq6OAfQvpus=
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
Expand Down Expand Up @@ -2010,8 +2011,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -2150,8 +2151,8 @@ golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,10 @@ spec:
- Tekton Pipelines: v0.65.1
- Tekton Triggers: v0.30.0
- Pipelines as Code: v0.28.1
- Pipelines as Code: v0.29.0
- Tekton Chains: v0.23.0
- Tekton Hub (tech-preview): v1.19.0
- Tekton Results (tech-preview): v0.12.2
- Tekton Results (tech-preview): v0.13.0
- Manual Approval Gate (tech-preview): v0.4.0
## Getting Started
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (i *InstallerSetClient) create(ctx context.Context, comp v1alpha1.TektonCom
func (i *InstallerSetClient) makeMainSets(ctx context.Context, comp v1alpha1.TektonComponent, manifest *mf.Manifest) ([]v1alpha1.TektonInstallerSet, error) {
staticManifest := manifest.Filter(mf.Not(mf.ByKind("Deployment")), mf.Not(mf.ByKind("Service")))
deploymentManifest := manifest.Filter(mf.Any(mf.ByKind("Deployment"), mf.ByKind("Service")))
statefulSetManifest := manifest.Filter(mf.Any(mf.ByKind("StatefulSet"), mf.ByKind("Service")))
statefulSetManifest := manifest.Filter(mf.Any(mf.ByKind("StatefulSet"), mf.Any(mf.ByKind("Deployment")), mf.ByKind("Service")))

kind := strings.ToLower(strings.TrimPrefix(i.resourceKind, "Tekton"))
staticName := fmt.Sprintf("%s-%s-%s-", kind, InstallerTypeMain, InstallerSubTypeStatic)
Expand Down
28 changes: 20 additions & 8 deletions pkg/reconciler/kubernetes/tektonpipeline/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const (
clusterResolverConfig = "cluster-resolver-config"
hubResolverConfig = "hubresolver-config"
gitResolverConfig = "git-resolver-config"
leaderElectionConfig = "config-leader-election-controller"
leaderElectionPipelineConfig = "config-leader-election-controller"
leaderElectionResolversConfig = "config-leader-election-resolvers"
pipelinesControllerDeployment = "tekton-pipelines-controller"
pipelinesControllerContainer = "tekton-pipelines-controller"
pipelinesRemoteResolversControllerDeployment = "tekton-pipelines-remote-resolvers"
Expand All @@ -53,6 +54,8 @@ const (

tektonPipelinesControllerName = "tekton-pipelines-controller"
tektonPipelinesServiceName = "tekton-pipelines-controller"
tektonRemoteResolversControllerName = "tekton-pipelines-remote-resolvers"
tektonRemoteResolversServiceName = "tekton-pipelines-remote-resolvers"
tektonPipelinesControllerStatefulServiceName = "STATEFUL_SERVICE_NAME"
tektonPipelinesControllerStatefulControllerOrdinal = "STATEFUL_CONTROLLER_ORDINAL"
)
Expand Down Expand Up @@ -83,13 +86,17 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
common.CopyConfigMap(hubResolverConfig, pipeline.Spec.HubResolverConfig),
common.CopyConfigMap(clusterResolverConfig, pipeline.Spec.ClusterResolverConfig),
common.CopyConfigMap(gitResolverConfig, pipeline.Spec.GitResolverConfig),
common.AddConfigMapValues(leaderElectionConfig, pipeline.Spec.Performance.PipelinePerformanceLeaderElectionConfig),
updatePerformanceFlagsInDeployment(pipeline),
common.AddConfigMapValues(leaderElectionPipelineConfig, pipeline.Spec.Performance.PipelinePerformanceLeaderElectionConfig),
common.AddConfigMapValues(leaderElectionResolversConfig, pipeline.Spec.Performance.PipelinePerformanceLeaderElectionConfig),
updatePerformanceFlagsInDeploymentAndLeaderConfigMap(pipeline, leaderElectionPipelineConfig, pipelinesControllerDeployment, pipelinesControllerContainer),
updatePerformanceFlagsInDeploymentAndLeaderConfigMap(pipeline, leaderElectionResolversConfig, pipelinesRemoteResolversControllerDeployment, pipelinesRemoteResolverControllerContainer),
updateResolverConfigEnvironmentsInDeployment(pipeline),
}
if pipeline.Spec.Performance.StatefulsetOrdinals != nil && *pipeline.Spec.Performance.StatefulsetOrdinals {
extra = append(extra, common.ConvertDeploymentToStatefulSet(tektonPipelinesControllerName, tektonPipelinesServiceName), common.AddStatefulEnvVars(
tektonPipelinesControllerName, tektonPipelinesServiceName, tektonPipelinesControllerStatefulServiceName, tektonPipelinesControllerStatefulControllerOrdinal))
extra = append(extra, common.ConvertDeploymentToStatefulSet(tektonRemoteResolversControllerName, tektonRemoteResolversServiceName), common.AddStatefulEnvVars(
tektonRemoteResolversControllerName, tektonRemoteResolversServiceName, tektonPipelinesControllerStatefulServiceName, tektonPipelinesControllerStatefulControllerOrdinal))
}

trns = append(trns, extra...)
Expand All @@ -108,10 +115,10 @@ func filterAndTransform(extension common.Extension) client.FilterAndTransform {
}
}

// updates performance flags/args into pipelines controller container
func updatePerformanceFlagsInDeployment(pipelineCR *v1alpha1.TektonPipeline) mf.Transformer {
// updates performance flags/args into deployment and container given as args
func updatePerformanceFlagsInDeploymentAndLeaderConfigMap(pipelineCR *v1alpha1.TektonPipeline, leaderConfig, deploymentName, containerName string) mf.Transformer {
return func(u *unstructured.Unstructured) error {
if u.GetKind() != "Deployment" || u.GetName() != pipelinesControllerDeployment {
if u.GetKind() != "Deployment" || u.GetName() != deploymentName {
return nil
}

Expand Down Expand Up @@ -151,7 +158,7 @@ func updatePerformanceFlagsInDeployment(pipelineCR *v1alpha1.TektonPipeline) mf.
labelKeys := getSortedKeys(leaderElectionConfigMapData)
for _, key := range labelKeys {
value := leaderElectionConfigMapData[key]
labelKey := fmt.Sprintf("%s.data.%s", leaderElectionConfig, key)
labelKey := fmt.Sprintf("%s.data.%s", leaderConfig, key)
podLabels[labelKey] = fmt.Sprintf("%v", value)
}
dep.Spec.Template.Labels = podLabels
Expand All @@ -170,13 +177,18 @@ func updatePerformanceFlagsInDeployment(pipelineCR *v1alpha1.TektonPipeline) mf.
flagKeys := getSortedKeys(flags)
// update performance arguments into target container
for containerIndex, container := range dep.Spec.Template.Spec.Containers {
if container.Name != pipelinesControllerContainer {
if container.Name != containerName {
continue
}
for _, flagKey := range flagKeys {
// update the arg name with "-" prefix
expectedArg := fmt.Sprintf("-%s", flagKey)
argStringValue := fmt.Sprintf("%v", flags[flagKey])
// skip deprecated disable-ha flag if not pipelinesControllerDeployment
// should be removed when the flag is removed from pipelines controller
if deploymentName != pipelinesControllerDeployment && flagKey == "disable-ha" {
continue
}
argUpdated := false
for argIndex, existingArg := range container.Args {
if strings.HasPrefix(existingArg, expectedArg) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/reconciler/kubernetes/tektonpipeline/transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (
"knative.dev/pkg/ptr"
)

func TestUpdatePerformanceFlagsInDeployment(t *testing.T) {
func TestUpdatePerformanceFlagsInDeploymentAndLeaderConfigMap(t *testing.T) {
pipelineCR := &v1alpha1.TektonPipeline{
ObjectMeta: metav1.ObjectMeta{
Name: "pipeline",
Expand Down Expand Up @@ -111,7 +111,7 @@ func TestUpdatePerformanceFlagsInDeployment(t *testing.T) {
assert.NilError(t, err)

// apply transformer
transformer := updatePerformanceFlagsInDeployment(pipelineCR)
transformer := updatePerformanceFlagsInDeploymentAndLeaderConfigMap(pipelineCR, leaderElectionPipelineConfig, pipelinesControllerDeployment, pipelinesControllerContainer)
err = transformer(ud)
assert.NilError(t, err)

Expand Down
42 changes: 20 additions & 22 deletions test/resources/statefulset.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,29 @@ func DeleteAndVerifyStatefulSet(t *testing.T, clients *utils.Clients, namespace,
t.Fatalf("No StatefulSet under the namespace %q was found", namespace)
}

// Delete the first StatefulSet and verify the operator recreates it
statefulSet := stsList.Items[0]
err = clients.KubeClient.AppsV1().StatefulSets(statefulSet.Namespace).Delete(context.TODO(), statefulSet.Name, metav1.DeleteOptions{})
if err != nil {
t.Fatalf("Failed to delete StatefulSet %s/%s: %v", statefulSet.Namespace, statefulSet.Name, err)
}

// Poll and wait for the StatefulSet to be recreated and ready
waitErr := wait.PollUntilContextTimeout(context.TODO(), utils.Interval, utils.Timeout, true, func(ctx context.Context) (bool, error) {
sts, err := clients.KubeClient.
AppsV1().StatefulSets(statefulSet.Namespace).Get(context.TODO(), statefulSet.Name, metav1.GetOptions{})
// Delete each StatefulSet and verify the operator recreates it
for _, statefulSet := range stsList.Items {
err = clients.KubeClient.AppsV1().StatefulSets(statefulSet.Namespace).Delete(context.TODO(), statefulSet.Name, metav1.DeleteOptions{})
if err != nil {
// If the StatefulSet is not found, we continue to wait for it to be recreated.
if apierrs.IsNotFound(err) {
return false, nil
}
return false, err
t.Fatalf("Failed to delete StatefulSet %s/%s: %v", statefulSet.Namespace, statefulSet.Name, err)
}
}
// Poll and wait for each StatefulSet to be recreated and ready
for _, statefulSet := range stsList.Items {
waitErr := wait.PollUntilContextTimeout(context.TODO(), utils.Interval, utils.Timeout, true, func(ctx context.Context) (bool, error) {
sts, err := clients.KubeClient.AppsV1().StatefulSets(statefulSet.Namespace).Get(context.TODO(), statefulSet.Name, metav1.GetOptions{})
if err != nil {
if apierrs.IsNotFound(err) {
return false, nil
}
return false, err
}
return IsStatefulSetAvailable(sts)
})

// Check if the StatefulSet is available
return IsStatefulSetAvailable(sts)
})

if waitErr != nil {
t.Fatalf("The StatefulSet %s/%s failed to reach the desired state: %v", statefulSet.Namespace, statefulSet.Name, waitErr)
if waitErr != nil {
t.Fatalf("The StatefulSet %s/%s failed to reach the desired state: %v", statefulSet.Namespace, statefulSet.Name, waitErr)
}
}
}

Expand Down
Loading

0 comments on commit 5406e20

Please sign in to comment.