Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App set UI refactor #4

Merged
merged 40 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4fc4c8d
initial
reggie-k May 6, 2023
a66b538
detailed view
reggie-k May 9, 2023
83b2b14
appset fix
reggie-k May 12, 2023
5f5c008
appset fix
reggie-k May 12, 2023
2266f2f
appset in settings with breadcrumbs
reggie-k May 25, 2023
33433d1
atempt to refactor
reggie-k May 29, 2023
698d712
cleanup
reggie-k Jun 13, 2023
af67baa
attempt to merge back into the file
reggie-k Jun 30, 2023
4a50440
feat: adding a autosync_enabled field to the argocd_app_info gauge (…
gerardocorea Jul 26, 2023
703df96
fix(controller): log failed attempts to update operation state (#14273)
crenshaw-dev Jul 26, 2023
e08840f
fix(server): handle PATCH in http/s server (#2677) (#14530)
mmerrill3 Jul 26, 2023
d04633b
fix: manifest generation error with null annotations (#14336) (#14680)
agaudreault Jul 26, 2023
f7c654a
Clean up repeated package import (#13889)
Fish-pro Jul 26, 2023
f1607fe
fix(sso): Set redirectURI for gitea, google, oauth Dex connectors (#1…
xianzheTM Jul 26, 2023
433ba36
feat(appset): Restrict scm provider urls (#14286)
speedfl Jul 27, 2023
89931c1
chore(deps): bump github.com/go-git/go-git/v5 from 5.8.0 to 5.8.1 (#1…
dependabot[bot] Jul 27, 2023
5f8fc55
chore(deps): bump github.com/aws/aws-sdk-go from 1.44.305 to 1.44.309…
dependabot[bot] Jul 27, 2023
9a47a76
fix(ui): display valuesobject if set (#14257)
blakepettersson Jul 27, 2023
1810c4c
fix: OCI dependency url can't contain part of repository (#14699)
alexmt Jul 27, 2023
74a07f1
docs: Add missing value (#14538)
soupdiver Jul 28, 2023
0a11089
[Bot] docs: Update Snyk reports (#14781)
github-actions[bot] Jul 30, 2023
c3970b4
chore(deps): bump github.com/aws/aws-sdk-go from 1.44.309 to 1.44.312…
dependabot[bot] Jul 31, 2023
6e3bd70
chore(deps): bump github.com/xanzy/go-gitlab from 0.88.0 to 0.89.0 (#…
dependabot[bot] Jul 31, 2023
58138ad
chore(deps): bump github.com/casbin/casbin/v2 from 2.72.1 to 2.73.0 (…
dependabot[bot] Jul 31, 2023
8b5118c
chore: add Autodesk to USERS.md (#14778)
GenPage Jul 31, 2023
2205b94
chore: Add query-scoped cluster URL in Cluster Secret E2E tests (#14446)
jgwest Jul 31, 2023
9e279d5
docs: Clarify that security policy covers last 3 versions (#14786)
kostis-codefresh Jul 31, 2023
2f84dac
fix(controller): cache deadlock on delete and re-add cluster (#14780)
Jul 31, 2023
e2ab450
chore: make `helm template` errors less verbose (#14772)
crenshaw-dev Jul 31, 2023
951b6b1
feat(deep-links): alias `application` as `apps` for consistency with …
crenshaw-dev Jul 31, 2023
92616e4
fix(ui): no hyphen for "create job" action + nice icon (#14776) (#14777)
crenshaw-dev Jul 31, 2023
5795ba7
fix: Correct broken `forever` option in pod logs viewer. Fixes #14762…
alexec Jul 31, 2023
f099aa7
docs: Update application.yaml (#14742)
crenshaw-dev Jul 31, 2023
49a3950
docs: add ignoreDifferences name and namespace fields (#14741)
crenshaw-dev Jul 31, 2023
0d99943
chore(deps): bump library/node from 20.4.0 to 20.5.0 (#14664)
dependabot[bot] Aug 1, 2023
be1b1ea
chore(deps): bump library/node from 20.4.0 to 20.5.0 in /ui-test (#14…
dependabot[bot] Aug 1, 2023
72754d7
docs: Update Controlling-Resource-Modification.md (#14751)
crenshaw-dev Aug 1, 2023
4a7e6ec
fix(notifications-catalog): Add nil check for notifications_catalog t…
ichbinfrog Aug 1, 2023
1a27de4
Refactor App Set UI - Regina's initial changes
keithchong Aug 1, 2023
c1e8098
Merge branch 'master' of [email protected]:argoproj/argo-cd.git into App…
keithchong Aug 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ WORKDIR /home/argocd
####################################################################################################
# Argo CD UI stage
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/node:20.4.0@sha256:b3ca7d32f0c12291df6e45a914d4ee60011a3fce4a978df5e609e356a4a2cb88 AS argocd-ui
FROM --platform=$BUILDPLATFORM docker.io/library/node:20.5.0@sha256:32ec50b65ac9572eda92baa6004a04dbbfc8021ea806fa62d37336183cad04e6 AS argocd-ui

WORKDIR /src
COPY ["ui/package.json", "ui/yarn.lock", "./"]
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
BIN_MODE=$(ARGOCD_BIN_MODE) \
ARGOCD_APPLICATION_NAMESPACES=argocd-e2e-external \
ARGOCD_APPLICATIONSET_CONTROLLER_NAMESPACES=argocd-e2e-external \
ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS=http://127.0.0.1:8341,http://127.0.0.1:8342,http://127.0.0.1:8343,http://127.0.0.1:8344 \
ARGOCD_E2E_TEST=true \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}

Expand Down
4 changes: 1 addition & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ impact on Argo CD before opening an issue at least roughly.

## Supported Versions

We currently support the most recent release (`N`, e.g. `1.8`) and the release
previous to the most recent one (`N-1`, e.g. `1.7`). With the release of
`N+1`, `N-1` drops out of support and `N` becomes `N-1`.
We currently support the last 3 minor versions of Argo CD with security and bug fixes.

We regularly perform patch releases (e.g. `1.8.5` and `1.7.12`) for the
supported versions, which will contain fixes for security vulnerabilities and
Expand Down
1 change: 1 addition & 0 deletions USERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [AppDirect](https://www.appdirect.com)
1. [Arctiq Inc.](https://www.arctiq.ca)
1. [ARZ Allgemeines Rechenzentrum GmbH](https://www.arz.at/)
2. [Autodesk](https://www.autodesk.com)
1. [Axual B.V.](https://axual.com)
1. [Back Market](https://www.backmarket.com)
1. [Baloise](https://www.baloise.com)
Expand Down
4 changes: 2 additions & 2 deletions applicationset/controllers/requeue_after_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ func TestRequeueAfter(t *testing.T) {
"List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(k8sClient, ctx, appClientset, "argocd"),
"Git": generators.NewGitGenerator(mockServer),
"SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, ""),
"SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, "", []string{""}),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd"),
"PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, ""),
"PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, "", []string{""}),
}

nestedGenerators := map[string]generators.Generator{
Expand Down
22 changes: 18 additions & 4 deletions applicationset/generators/pull_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ type PullRequestGenerator struct {
selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
auth SCMAuthProviders
scmRootCAPath string
allowedSCMProviders []string
}

func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string) Generator {
func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string, allowedScmProviders []string) Generator {
g := &PullRequestGenerator{
client: client,
auth: auth,
scmRootCAPath: scmRootCAPath,
client: client,
auth: auth,
scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedScmProviders,
}
g.selectServiceProviderFunc = g.selectServiceProvider
return g
Expand Down Expand Up @@ -120,10 +122,16 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
// selectServiceProvider selects the provider to get pull requests from the configuration
func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, generatorConfig *argoprojiov1alpha1.PullRequestGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
if generatorConfig.Github != nil {
if !ScmProviderAllowed(applicationSetInfo, generatorConfig.Github.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", generatorConfig.Github.API)
}
return g.github(ctx, generatorConfig.Github, applicationSetInfo)
}
if generatorConfig.GitLab != nil {
providerConfig := generatorConfig.GitLab
if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API)
}
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %v", err)
Expand All @@ -132,6 +140,9 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
}
if generatorConfig.Gitea != nil {
providerConfig := generatorConfig.Gitea
if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", generatorConfig.Gitea.API)
}
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %v", err)
Expand All @@ -140,6 +151,9 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
}
if generatorConfig.BitbucketServer != nil {
providerConfig := generatorConfig.BitbucketServer
if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API)
}
if providerConfig.BasicAuth != nil {
password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
if err != nil {
Expand Down
77 changes: 77 additions & 0 deletions applicationset/generators/pull_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,80 @@ func TestPullRequestGetSecretRef(t *testing.T) {
})
}
}

func TestAllowedSCMProviderPullRequest(t *testing.T) {
cases := []struct {
name string
providerConfig *argoprojiov1alpha1.PullRequestGenerator
expectedError string
}{
{
name: "Error Github",
providerConfig: &argoprojiov1alpha1.PullRequestGenerator{
Github: &argoprojiov1alpha1.PullRequestGeneratorGithub{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Gitlab",
providerConfig: &argoprojiov1alpha1.PullRequestGenerator{
GitLab: &argoprojiov1alpha1.PullRequestGeneratorGitLab{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Gitea",
providerConfig: &argoprojiov1alpha1.PullRequestGenerator{
Gitea: &argoprojiov1alpha1.PullRequestGeneratorGitea{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Bitbucket",
providerConfig: &argoprojiov1alpha1.PullRequestGenerator{
BitbucketServer: &argoprojiov1alpha1.PullRequestGeneratorBitbucketServer{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "failed to select pull request service provider: scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
}

for _, testCase := range cases {
testCaseCopy := testCase

t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()

pullRequestGenerator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{
"github.myorg.com",
"gitlab.myorg.com",
"gitea.myorg.com",
"bitbucket.myorg.com",
"azuredevops.myorg.com",
})

applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
PullRequest: testCaseCopy.providerConfig,
}},
},
}

_, err := pullRequestGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo)

assert.Error(t, err, "Must return an error")
assert.Equal(t, testCaseCopy.expectedError, err.Error())
})
}
}
50 changes: 45 additions & 5 deletions applicationset/generators/scm_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import (
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

log "github.com/sirupsen/logrus"

"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/argoproj/argo-cd/v2/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)

Expand All @@ -26,18 +29,20 @@ type SCMProviderGenerator struct {
// Testing hooks.
overrideProvider scm_provider.SCMProviderService
SCMAuthProviders
scmRootCAPath string
scmRootCAPath string
allowedSCMProviders []string
}

type SCMAuthProviders struct {
GitHubApps github_app_auth.Credentials
}

func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string) Generator {
func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string, allowedSCMProviders []string) Generator {
return &SCMProviderGenerator{
client: client,
SCMAuthProviders: providers,
scmRootCAPath: scmRootCAPath,
client: client,
SCMAuthProviders: providers,
scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedSCMProviders,
}
}

Expand All @@ -60,6 +65,26 @@ func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.A
return &appSetGenerator.SCMProvider.Template
}

func ScmProviderAllowed(applicationSetInfo *argoprojiov1alpha1.ApplicationSet, url string, allowedScmProviders []string) bool {
if url == "" || len(allowedScmProviders) == 0 {
return true
}

for _, allowedScmProvider := range allowedScmProviders {
if url == allowedScmProvider {
return true
}
}

log.WithFields(log.Fields{
common.SecurityField: common.SecurityMedium,
"applicationset": applicationSetInfo.Name,
"appSetNamespace": applicationSetInfo.Namespace,
}).Debugf("attempted to use disallowed SCM %q", url)

return false
}

func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) {
if appSetGenerator == nil {
return nil, EmptyAppSetGeneratorError
Expand All @@ -77,12 +102,18 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
if g.overrideProvider != nil {
provider = g.overrideProvider
} else if providerConfig.Github != nil {
if !ScmProviderAllowed(applicationSetInfo, providerConfig.Github.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Github.API)
}
var err error
provider, err = g.githubProvider(ctx, providerConfig.Github, applicationSetInfo)
if err != nil {
return nil, fmt.Errorf("scm provider: %w", err)
}
} else if providerConfig.Gitlab != nil {
if !ScmProviderAllowed(applicationSetInfo, providerConfig.Gitlab.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Gitlab.API)
}
token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Gitlab token: %v", err)
Expand All @@ -92,6 +123,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
return nil, fmt.Errorf("error initializing Gitlab service: %v", err)
}
} else if providerConfig.Gitea != nil {
if !ScmProviderAllowed(applicationSetInfo, providerConfig.Gitea.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.Gitea.API)
}
token, err := g.getSecretRef(ctx, providerConfig.Gitea.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Gitea token: %v", err)
Expand All @@ -102,6 +136,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
}
} else if providerConfig.BitbucketServer != nil {
providerConfig := providerConfig.BitbucketServer
if !ScmProviderAllowed(applicationSetInfo, providerConfig.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.API)
}
var scmError error
if providerConfig.BasicAuth != nil {
password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
Expand All @@ -116,6 +153,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
return nil, fmt.Errorf("error initializing Bitbucket Server service: %v", scmError)
}
} else if providerConfig.AzureDevOps != nil {
if !ScmProviderAllowed(applicationSetInfo, providerConfig.AzureDevOps.API, g.allowedSCMProviders) {
return nil, fmt.Errorf("scm provider not allowed: %s", providerConfig.AzureDevOps.API)
}
token, err := g.getSecretRef(ctx, providerConfig.AzureDevOps.AccessTokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Azure Devops access token: %v", err)
Expand Down
86 changes: 86 additions & 0 deletions applicationset/generators/scm_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,89 @@ func TestSCMProviderGenerateParams(t *testing.T) {
})
}
}

func TestAllowedSCMProvider(t *testing.T) {
cases := []struct {
name string
providerConfig *argoprojiov1alpha1.SCMProviderGenerator
expectedError string
}{
{
name: "Error Github",
providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
Github: &argoprojiov1alpha1.SCMProviderGeneratorGithub{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Gitlab",
providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
Gitlab: &argoprojiov1alpha1.SCMProviderGeneratorGitlab{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Gitea",
providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
Gitea: &argoprojiov1alpha1.SCMProviderGeneratorGitea{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error Bitbucket",
providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
BitbucketServer: &argoprojiov1alpha1.SCMProviderGeneratorBitbucketServer{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
{
name: "Error AzureDevops",
providerConfig: &argoprojiov1alpha1.SCMProviderGenerator{
AzureDevOps: &argoprojiov1alpha1.SCMProviderGeneratorAzureDevOps{
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: "scm provider not allowed: https://myservice.mynamespace.svc.cluster.local",
},
}

for _, testCase := range cases {
testCaseCopy := testCase

t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()

scmGenerator := &SCMProviderGenerator{allowedSCMProviders: []string{
"github.myorg.com",
"gitlab.myorg.com",
"gitea.myorg.com",
"bitbucket.myorg.com",
"azuredevops.myorg.com",
}}

applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: argoprojiov1alpha1.ApplicationSetSpec{
Generators: []argoprojiov1alpha1.ApplicationSetGenerator{{
SCMProvider: testCaseCopy.providerConfig,
}},
},
}

_, err := scmGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo)

assert.Error(t, err, "Must return an error")
assert.Equal(t, testCaseCopy.expectedError, err.Error())
})
}
}
Loading
Loading