Compare commits

...

21 Commits

Author SHA1 Message Date
argo-bot
ac8b7df946 Bump version to 2.3.4 2022-05-18 11:32:27 +00:00
argo-bot
5d515b8423 Bump version to 2.3.4 2022-05-18 11:32:11 +00:00
jannfis
69dcee049e Merge pull request from GHSA-r642-gv9p-2wjj
Signed-off-by: jannfis <jann@mistrust.net>

Co-authored-by: Michael Crenshaw <michael@crenshaw.dev>

Co-authored-by: Michael Crenshaw <michael@crenshaw.dev>
2022-05-18 13:16:21 +02:00
Michael Crenshaw
d36d95dc9f Merge pull request from GHSA-6gcg-hp2x-q54h
* fix: do not allow symlinks from directory-type applications

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

* chore: add new util file

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

* chore: lint

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

* chore: use t.TempDir for simpler tests

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

* address comments

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>
2022-05-18 13:13:41 +02:00
jannfis
df79d7db1d Merge pull request from GHSA-xmg8-99r8-jc2j
Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

Co-authored-by: Michael Crenshaw <michael@crenshaw.dev>
2022-05-18 13:06:31 +02:00
Daniel Helfand
7165431a84 fix: allow cli/ui to follow logs (#8987) (#9065)
Signed-off-by: Daniel Helfand <helfand.4@gmail.com>
2022-04-11 11:34:42 -04:00
Michael Crenshaw
13ec3f43d8 chore: upgrade to go 1.17.8 (#8866) (#9004)
* chore: upgrade to go 1.17.8

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>

* chore: use 1.17 so it's always latest in the series

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>
2022-04-05 08:45:56 -07:00
Alexander Matyushentsev
eea93c5103 fix: fix broken monaco editor collapse icons (#8709)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-03-30 14:19:23 -07:00
pasha-codefresh
2cc81959b4 fix: Fix docs build error (#8895)
* work with specific jinja version

Signed-off-by: pashavictorovich <pavel@codefresh.io>
2022-03-30 11:29:28 -07:00
argo-bot
07ac038a8f Bump version to 2.3.3 2022-03-29 23:56:04 +00:00
argo-bot
3828da7f8c Bump version to 2.3.3 2022-03-29 23:55:46 +00:00
Alexander Matyushentsev
3bd9121545 docs: reflect v2.3 release changes in roadmap.md (#8747)
docs: reflect v2.3 release changes in roadmap.md (#8747)

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-03-29 16:49:52 -07:00
Ishita Sequeira
df6e7c169e docs: update v2.4+ roadmap items (#8593)
Signed-off-by: ishitasequeira <isequeir@redhat.com>
2022-03-29 16:49:48 -07:00
Alexander Matyushentsev
25b01666f0 fix: bump gitops engine version to v0.6.2
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-03-29 15:20:54 -07:00
Michael Crenshaw
9d6e6d84de fix: prevent excessive repo-server disk usage for large repos (#8845) (#8897)
fix: prevent excessive repo-server disk usage for large repos (#8845) (#8897)

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>
2022-03-29 15:14:10 -07:00
jannfis
7f9ff6e8c3 fix: Set QPS and burst rate for resource ops client (#8915)
* fix: Set QPS and burst rate for resource ops client

Signed-off-by: jannfis <jann@mistrust.net>
2022-03-29 07:34:31 +00:00
argo-bot
ecc2af9dca Bump version to 2.3.2 2022-03-23 00:32:13 +00:00
argo-bot
c5b0279050 Bump version to 2.3.2 2022-03-23 00:31:57 +00:00
Michael Crenshaw
6df17e7c56 chore: fix imports (#8859)
Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>
2022-03-22 12:55:09 -07:00
Michael Crenshaw
e55ecf9107 chore: remove lint-docs CI task (#8722) (#8858)
* chore: remove lint-docs CI task

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>

* chore: remove not longer necessary url-allow-list

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>

Co-authored-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-03-22 12:54:37 -07:00
Alexander Matyushentsev
21f208f17e Merge pull request from GHSA-2f5v-8r3f-8pww
* fix: application resource APIs must enforce project restrictions

Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>

* Fix unit tests

Signed-off-by: jannfis <jann@mistrust.net>

Co-authored-by: jannfis <jann@mistrust.net>
2022-03-22 10:57:31 -07:00
43 changed files with 1010 additions and 315 deletions

View File

@@ -12,7 +12,7 @@ on:
env:
# Golang version to use across CI steps
GOLANG_VERSION: '1.17.6'
GOLANG_VERSION: '1.17'
jobs:
check-go:
@@ -427,20 +427,3 @@ jobs:
name: e2e-server-k8s${{ matrix.k3s-version }}.log
path: /tmp/e2e-server.log
if: ${{ failure() }}
lint-docs:
name: Lint docs
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install -r docs/requirements.txt
- name: Lint docs
run: |
make lint-docs

View File

@@ -10,7 +10,7 @@ on:
types: [ labeled, unlabeled, opened, synchronize, reopened ]
env:
GOLANG_VERSION: '1.17.6'
GOLANG_VERSION: '1.17'
jobs:
publish:

View File

@@ -12,7 +12,7 @@ on:
- '!release-v0*'
env:
GOLANG_VERSION: '1.17.6'
GOLANG_VERSION: '1.17'
jobs:
prepare-release:

View File

@@ -4,7 +4,7 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:21.10
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM docker.io/library/golang:1.17.6 as builder
FROM docker.io/library/golang:1.17 as builder
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
@@ -102,7 +102,7 @@ RUN HOST_ARCH='amd64' NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTION
####################################################################################################
# Argo CD Build stage which performs the actual build of Argo CD binaries
####################################################################################################
FROM docker.io/library/golang:1.17.6 as argocd-build
FROM docker.io/library/golang:1.17 as argocd-build
WORKDIR /go/src/github.com/argoproj/argo-cd

View File

@@ -509,10 +509,6 @@ serve-docs-local:
serve-docs:
docker run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs ${MKDOCS_DOCKER_IMAGE} serve -a 0.0.0.0:8000
.PHONY: lint-docs
lint-docs:
# https://github.com/dkhamsing/awesome_bot
find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --allow-timeout --allow-ssl --allow 502,500,429,400 --white-list `cat docs/url-allow-list | grep -v "#" | tr "\n" ','` --skip-save-results --
# Verify that kubectl can connect to your K8s cluster from Docker
.PHONY: verify-kube-connect

View File

@@ -1 +1 @@
2.3.1
2.3.4

View File

@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
@@ -421,8 +422,12 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
},
})
} else {
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) {
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) bool {
if !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
return false
}
nodes = append(nodes, child)
return true
})
if err != nil {
return nil, err
@@ -432,16 +437,18 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
orphanedNodes := make([]appv1.ResourceNode, 0)
for k := range orphanedNodesMap {
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k, proj) {
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) bool {
belongToAnotherApp := false
if appName != "" {
if _, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName); exists && err == nil {
belongToAnotherApp = true
}
}
if !belongToAnotherApp {
orphanedNodes = append(orphanedNodes, child)
if belongToAnotherApp || !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
return false
}
orphanedNodes = append(orphanedNodes, child)
return true
})
if err != nil {
return nil, err
@@ -1291,6 +1298,13 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
app.Status.Health.Status = health.HealthStatusUnknown
ctrl.persistAppStatus(origApp, &app.Status)
if err := ctrl.cache.SetAppResourcesTree(app.Name, &appv1.ApplicationTree{}); err != nil {
log.Warnf("failed to set app resource tree: %v", err)
}
if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
log.Warnf("failed to set app managed resources tree: %v", err)
}
return
}

View File

@@ -136,12 +136,12 @@ func newFakeController(data *fakeData) *ApplicationController {
mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil)
mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
key := args[1].(kube.ResourceKey)
action := args[2].(func(child argoappv1.ResourceNode, appName string))
action := args[2].(func(child argoappv1.ResourceNode, appName string) bool)
appName := ""
if res, ok := data.namespacedResources[key]; ok {
appName = res.AppName
}
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
_ = action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
}).Return(nil)
return ctrl
}

View File

@@ -104,7 +104,7 @@ type LiveStateCache interface {
// Returns synced cluster cache
GetClusterCache(server string) (clustercache.ClusterCache, error)
// Executes give callback against resource specified by the key and all its children
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error
// Returns state of live nodes which correspond for target nodes of specified application.
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
// IterateResources iterates all resource stored in cache
@@ -437,13 +437,13 @@ func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool,
return clusterInfo.IsNamespaced(gk)
}
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
return err
}
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
action(asResourceNode(resource), getApp(resource, namespaceResources))
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) bool {
return action(asResourceNode(resource), getApp(resource, namespaceResources))
})
return nil
}

View File

@@ -176,11 +176,11 @@ func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool
}
// IterateHierarchy provides a mock function with given fields: server, key, action
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string)) error {
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string) bool) error {
ret := _m.Called(server, key, action)
var r0 error
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string) bool) error); ok {
r0 = rf(server, key, action)
} else {
r0 = ret.Error(0)

View File

@@ -1,4 +1,5 @@
mkdocs==1.2.3
mkdocs-material==7.1.7
markdown_include==0.6.0
pygments==2.7.4
pygments==2.7.4
jinja2===3.0.3

View File

@@ -1,26 +1,29 @@
# Roadmap
- [Roadmap](#roadmap)
- [v2.3](#v23)
- [Merge Argo CD Notifications into Argo CD](#merge-argo-cd-notifications-into-argo-cd)
- [Merge ApplicationSet controller into Argo CD](#merge-applicationset-controller-into-argo-cd)
- [Compact resources tree](#compact-resources-tree)
- [Maintain difference in cluster and git values for specific fields](#maintain-difference-in-cluster-and-git-values-for-specific-fields)
- [ARM images and CLI binary](#arm-images-and-cli-binary)
- [v2.4](#v24)
- [Server side apply](#server-side-apply)
- [v2.4 and beyond](#v24-and-beyond)
- [First class support for ApplicationSet resources](#first-class-support-for-applicationset-resources)
- [Input Forms UI Refresh](#input-forms-ui-refresh)
- [Merge Argo CD Image Updater into Argo CD](#merge-argo-cd-image-updater-into-argo-cd)
- [Web Shell](#web-shell)
- [Helm values from external repo](#helm-values-from-external-repo)
- [Support multiple sources for an Application](#support-multiple-sources-for-an-application)
- [Config Management Tools Enhancements: Parametrization & Security Improvements](#config-management-tools-enhancements-parametrization--security-improvements)
- [v2.5 and beyond](#v25-and-beyond)
- [Config Management Tools Enhancements: UI/CLI](#config-management-tools-enhancements-uicli)
- [First class support for ApplicationSet resources](#first-class-support-for-applicationset-resources)
- [Merge Argo CD Image Updater into Argo CD](#merge-argo-cd-image-updater-into-argo-cd)
- [Sharding application controller](#sharding-application-controller)
- [Add support for secrets in Application parameters](#add-support-for-secrets-in-application-parameters)
- [Config Management Tools Integrations UI/CLI](#config-management-tools-integrations-uicli)
- [Allow specifying parent/child relationships in config](#allow-specifying-parentchild-relationships-in-config)
- [Dependencies between applications](#dependencies-between-applications)
- [Multi-tenancy improvements](#multi-tenancy-improvements)
- [GitOps Engine Enhancements](#gitops-engine-enhancements)
- [Completed](#completed)
- [✅ Merge Argo CD Notifications into Argo CD](#-merge-argo-cd-notifications-into-argo-cd)
- [✅ Merge ApplicationSet controller into Argo CD](#-merge-applicationset-controller-into-argo-cd)
- [✅ Compact resources tree](#-compact-resources-tree)
- [✅ Maintain difference in cluster and git values for specific fields](#-maintain-difference-in-cluster-and-git-values-for-specific-fields)
- [✅ ARM images and CLI binary](#-arm-images-and-cli-binary)
- [✅ Config Management Tools Integrations (proposal)](#-config-management-tools-integrations-proposal)
- [✅ Argo CD Extensions (proposal)](#-argo-cd-extensions-proposal)
- [✅ Project scoped repository and clusters (proposal)](#-project-scoped-repository-and-clusters-proposal)
@@ -34,49 +37,19 @@
- [✅ Automated Registry Monitoring](#-automated-registry-monitoring)
- [✅ Projects Enhancements](#-projects-enhancements)
## v2.3
## v2.4
> ETA: Feb 2021
### Merge Argo CD Notifications into Argo CD
The [Argo CD Notifications](https://github.com/argoproj-labs/argocd-notifications) should be merged into Argo CD and available out-of-the-box: [#7350](https://github.com/argoproj/argo-cd/issues/7350)
### Merge ApplicationSet controller into Argo CD
The ApplicationSet functionality is available in Argo CD out-of-the-box ([#7351](https://github.com/argoproj/argo-cd/issues/7351)).
### Compact resources tree
An ability to collaps leaf resources tree to improve visualization of very large applications: [#7349](https://github.com/argoproj/argo-cd/issues/7349)
### Maintain difference in cluster and git values for specific fields
The feature allows to avoid updating fields excluded from diffing ([#2913](https://github.com/argoproj/argo-cd/issues/2913)).
### ARM images and CLI binary
The release workflow should build and publish ARM images and CLI binaries: ([#4211](https://github.com/argoproj/argo-cd/issues/4211))
> ETA: May 2022
### Server side apply
Support using [server side apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) during application syncing
[#2267](https://github.com/argoproj/argo-cd/issues/2267)
## v2.4 and beyond
### First class support for ApplicationSet resources
The Argo CD UI/CLI/API allows to manage ApplicationSet resources same as Argo CD Applications ([#7352](https://github.com/argoproj/argo-cd/issues/7352)).
### Input Forms UI Refresh
Improved design of the input forms in Argo CD Web UI: https://www.figma.com/file/IIlsFqqmM5UhqMVul9fQNq/Argo-CD?node-id=0%3A1
### Merge Argo CD Image Updater into Argo CD
The [Argo CD Image Updater](https://github.com/argoproj-labs/argocd-image-updater) should be merged into Argo CD and available out-of-the-box: [#7385](https://github.com/argoproj/argo-cd/issues/7385)
### Web Shell
Exec into the Kubernetes Pod right from Argo CD Web UI! [#4351](https://github.com/argoproj/argo-cd/issues/4351)
@@ -85,15 +58,40 @@ Exec into the Kubernetes Pod right from Argo CD Web UI! [#4351](https://github.c
The feature allows combining of-the-shelf Helm chart and value file in Git repository ([#2789](https://github.com/argoproj/argo-cd/issues/2789))
### Support multiple sources for an Application
Support more than one source for creating an Application [#8322](https://github.com/argoproj/argo-cd/pull/8322).
### Config Management Tools Enhancements: Parametrization & Security Improvements
The continuation of the Config Management Tools of [proposal](https://github.com/argoproj/argo-cd/blob/master/docs/proposals/parameterized-config-management-plugins.md).
The Argo config management plugin configuration allows users to specify the accepted parameters, default values to eventually power UI and CLI.
Additionally, plugins implementation should provide better Argo CD tenant isolation and security.
## v2.5 and beyond
### Config Management Tools Enhancements: UI/CLI
The Argo CD should provide a first-class experience for configured third-party config management tools. User should be able to view supported parameters,
observe default parameter values and override them.
### First class support for ApplicationSet resources
The Argo CD UI/CLI/API allows to manage ApplicationSet resources same as Argo CD Applications ([#7352](https://github.com/argoproj/argo-cd/issues/7352)).
### Merge Argo CD Image Updater into Argo CD
The [Argo CD Image Updater](https://github.com/argoproj-labs/argocd-image-updater) should be merged into Argo CD and available out-of-the-box: [#7385](https://github.com/argoproj/argo-cd/issues/7385)
### Sharding application controller
Application controller to scale automatically to provide high availability[#8340](https://github.com/argoproj/argo-cd/issues/8340).
### Add support for secrets in Application parameters
The feature allows referencing secrets in Application parameters. [#1786](https://github.com/argoproj/argo-cd/issues/1786).
### Config Management Tools Integrations UI/CLI
The continuation of the Config Management Tools of [proposal](https://github.com/argoproj/argo-cd/pull/5927). The Argo CD UI/CLI
should provide first class experience for configured third-party config management tools: [#5734](https://github.com/argoproj/argo-cd/issues/5734).
### Allow specifying parent/child relationships in config
The feature [#5082](https://github.com/argoproj/argo-cd/issues/5082) allows configuring parent/child relationships between resources. This allows to correctly
@@ -123,13 +121,32 @@ A lot of Argo CD features are still not available in GitOps engine. The followin
## Completed
### ✅ Merge Argo CD Notifications into Argo CD
The [Argo CD Notifications](https://github.com/argoproj-labs/argocd-notifications) should be merged into Argo CD and available out-of-the-box: [#7350](https://github.com/argoproj/argo-cd/issues/7350)
### ✅ Merge ApplicationSet controller into Argo CD
The ApplicationSet functionality is available in Argo CD out-of-the-box ([#7351](https://github.com/argoproj/argo-cd/issues/7351)).
### ✅ Compact resources tree
An ability to collaps leaf resources tree to improve visualization of very large applications: [#7349](https://github.com/argoproj/argo-cd/issues/7349)
### ✅ Maintain difference in cluster and git values for specific fields
The feature allows to avoid updating fields excluded from diffing ([#2913](https://github.com/argoproj/argo-cd/issues/2913)).
### ✅ ARM images and CLI binary
The release workflow should build and publish ARM images and CLI binaries: ([#4211](https://github.com/argoproj/argo-cd/issues/4211))
### ✅ Config Management Tools Integrations ([proposal](https://github.com/argoproj/argo-cd/pull/5927))
The community likes the first class support of Helm, Kustomize and keeps requesting support for more tools.
Argo CD provides a mechanism to integrate with any config management tool. We need to investigate why
it is not enough and implement missing features.
### ✅ Argo CD Extensions ([proposal](https://github.com/argoproj/argo-cd/pull/6240))
Argo CD supports customizing handling of Kubernetes resources via diffing customizations,

View File

@@ -1,70 +0,0 @@
# a list of sites we ignore when checking for broken links in mkdocs
10.97.164.88
192.168.0.20
argocd.example.com
api.github.com/user
cd.apps.argoproj.io
docker-build
docker-build:443
git.example.com
git.example.com:443
github.com/argoproj/another-private-repo
github.com/argoproj/my-private-repository
github.com/argoproj/other-private-repo
github.com/argoproj/private-repo
github.com/otherproj/another-private-repo
ksonnet.io
raw.githubusercontent.com/argoproj/argo-cd
repo.example.com
repo.example.com:443
server.example.com
kubernetes.default.svc
kubernetes.default.svc:443
localhost:4000
localhost:6443
localhost:8080
localhost:8085
mycluster.com
storage.googleapis.com
ui.argocd.yourorganization.net
ui.argocd.yourorganization.net:443
your-kubernetes-cluster-addr
yourorganization.oktapreview.com
yourorganization.oktapreview.com:443
example-OIDC-provider.com
argocd-dex-server:5556
ghe.example.com
proxy-server-url:8888
keycloak.example.com
argocd.myproject.com
argocd.apps.domain.com
k8sou.apps.192-168-2-144.nip.io
your.argoingress.address
your.domain
external.path.to.argocd.io
my-argo-cd-url
my-login-url
login.microsoftonline.com/xxxxx
accounts.google.com/o/saml2/idp?idpid=Abcde0
accounts.google.com/o/saml2?idpid=Abcde0
sso-url
google-entity-id
github.com/argoproj/argo-cd/manifests/crds
example.com
form.example.com
grafana.example.com
10.5.39.39
chat.googleapis.com/v1/spaces/
mattermost.example.com
my-grafana.com
github.my-company.com
1.2.3.4
2.4.6.8
9.8.7.6
ghe.example.com
12.34.567.89
192.168.99.100:8443
github.com/yourghuser/argo-cd
github.com/argoproj/argo-cd/releases/download/
https://github.com/hayorov/helm-gcs.git;
grafana.apps.argoproj.io

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
github.com/alicebob/miniredis v2.5.0+incompatible
github.com/alicebob/miniredis/v2 v2.14.2
github.com/argoproj/gitops-engine v0.6.0
github.com/argoproj/gitops-engine v0.6.2
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998
github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0
github.com/bombsimon/logrusr/v2 v2.0.1

4
go.sum
View File

@@ -125,8 +125,8 @@ github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/appscode/go v0.0.0-20190808133642-1d4ef1f1c1e0/go.mod h1:iy07dV61Z7QQdCKJCIvUoDL21u6AIceRhZzyleh2ymc=
github.com/argoproj/gitops-engine v0.6.0 h1:Tnh6kUUVuBV0m3gueYIymAeErWl9XNN9O9JcOoNM0vU=
github.com/argoproj/gitops-engine v0.6.0/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
github.com/argoproj/gitops-engine v0.6.2 h1:hM+pQeplCeIPAvfAmr1f91+ykxqaU0GAzuxVujqlKHM=
github.com/argoproj/gitops-engine v0.6.2/go.mod h1:pRgVpLW7pZqf7n3COJ7UcDepk4cI61LAcJd64Q3Jq/c=
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998 h1:V9RDg+IZeebnm3XjkfkbN07VM21Fu1Cy/RJNoHO++VM=
github.com/argoproj/notifications-engine v0.3.1-0.20220127183449-91deed20b998/go.mod h1:5mKv7zEgI3NO0L+fsuRSwBSY9EIXSuyIsDND8O8TTIw=
github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 h1:Cfp7rO/HpVxnwlRqJe0jHiBbZ77ZgXhB6HWlYD02Xdc=

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.3.1
newTag: v2.3.4
resources:
- ./application-controller
- ./dex

View File

@@ -9692,7 +9692,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -9741,7 +9741,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
name: copyutil
volumeMounts:
- mountPath: /var/run/argocd
@@ -9906,7 +9906,7 @@ spec:
key: controller.default.cache.expiration
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -11,4 +11,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.3.1
newTag: v2.3.4

View File

@@ -11,7 +11,7 @@ patchesStrategicMerge:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: v2.3.1
newTag: v2.3.4
resources:
- ../../base/application-controller
- ../../base/dex

View File

@@ -10516,7 +10516,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -10549,7 +10549,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -10782,7 +10782,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -10831,7 +10831,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
name: copyutil
volumeMounts:
- mountPath: /var/run/argocd
@@ -11058,7 +11058,7 @@ spec:
key: server.http.cookie.maxnumber
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -11254,7 +11254,7 @@ spec:
key: controller.default.cache.expiration
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -7812,7 +7812,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -7845,7 +7845,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -8078,7 +8078,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -8127,7 +8127,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
name: copyutil
volumeMounts:
- mountPath: /var/run/argocd
@@ -8354,7 +8354,7 @@ spec:
key: server.http.cookie.maxnumber
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -8550,7 +8550,7 @@ spec:
key: controller.default.cache.expiration
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -9886,7 +9886,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -9919,7 +9919,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -10116,7 +10116,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -10165,7 +10165,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
name: copyutil
volumeMounts:
- mountPath: /var/run/argocd
@@ -10388,7 +10388,7 @@ spec:
key: server.http.cookie.maxnumber
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -10578,7 +10578,7 @@ spec:
key: controller.default.cache.expiration
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -7182,7 +7182,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -7215,7 +7215,7 @@ spec:
containers:
- command:
- argocd-notifications
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -7412,7 +7412,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -7461,7 +7461,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
name: copyutil
volumeMounts:
- mountPath: /var/run/argocd
@@ -7684,7 +7684,7 @@ spec:
key: server.http.cookie.maxnumber
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -7874,7 +7874,7 @@ spec:
key: controller.default.cache.expiration
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:v2.3.1
image: quay.io/argoproj/argocd:v2.3.4
imagePullPolicy: Always
livenessProbe:
httpGet:

View File

@@ -313,11 +313,15 @@ func (proj AppProject) IsGroupKindPermitted(gk schema.GroupKind, namespaced bool
// IsLiveResourcePermitted returns whether a live resource found in the cluster is permitted by an AppProject
func (proj AppProject) IsLiveResourcePermitted(un *unstructured.Unstructured, server string, name string) bool {
if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace() != "") {
return proj.IsResourcePermitted(un.GroupVersionKind().GroupKind(), un.GetNamespace(), ApplicationDestination{Server: server, Name: name})
}
func (proj AppProject) IsResourcePermitted(groupKind schema.GroupKind, namespace string, dest ApplicationDestination) bool {
if !proj.IsGroupKindPermitted(groupKind, namespace != "") {
return false
}
if un.GetNamespace() != "" {
return proj.IsDestinationPermitted(ApplicationDestination{Server: server, Namespace: un.GetNamespace(), Name: name})
if namespace != "" {
return proj.IsDestinationPermitted(ApplicationDestination{Server: dest.Server, Name: dest.Name, Namespace: namespace})
}
return true
}

View File

@@ -48,17 +48,17 @@ var (
K8sMaxIdleConnections = env.ParseNumFromEnv(EnvK8sClientMaxIdleConnections, 500, 0, math.MaxInt32)
// K8sTLSHandshakeTimeout defines the maximum duration to wait for a TLS handshake to complete
K8sTLSHandshakeTimeout = env.ParseDurationFromEnv(EnvK8sTLSHandshakeTimeout, 10*time.Second, 0, math.MaxInt32)
K8sTLSHandshakeTimeout = env.ParseDurationFromEnv(EnvK8sTLSHandshakeTimeout, 10*time.Second, 0, math.MaxInt32*time.Second)
// K8sTCPTimeout defines the TCP timeout to use when performing K8s API requests
K8sTCPTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 30*time.Second, 0, math.MaxInt32)
K8sTCPTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 30*time.Second, 0, math.MaxInt32*time.Second)
// K8sTCPKeepAlive defines the interval for sending TCP keep alive to K8s API server
K8sTCPKeepAlive = env.ParseDurationFromEnv(EnvK8sTCPKeepAlive, 30*time.Second, 0, math.MaxInt32)
K8sTCPKeepAlive = env.ParseDurationFromEnv(EnvK8sTCPKeepAlive, 30*time.Second, 0, math.MaxInt32*time.Second)
// K8sTCPIdleConnTimeout defines the duration for keeping idle TCP connections to the K8s API server
K8sTCPIdleConnTimeout = env.ParseDurationFromEnv(EnvK8sTCPIdleConnTimeout, 5*time.Minute, 0, math.MaxInt32)
K8sTCPIdleConnTimeout = env.ParseDurationFromEnv(EnvK8sTCPIdleConnTimeout, 5*time.Minute, 0, math.MaxInt32*time.Second)
// K8sServerSideTimeout defines which server side timeout to send with each API request
K8sServerSideTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 32*time.Second, 0, math.MaxInt32)
K8sServerSideTimeout = env.ParseDurationFromEnv(EnvK8sTCPTimeout, 0, 0, math.MaxInt32*time.Second)
)

View File

@@ -2477,6 +2477,8 @@ func (c *Cluster) RawRestConfig() *rest.Config {
panic(fmt.Sprintf("Unable to create K8s REST config: %v", err))
}
config.Timeout = K8sServerSideTimeout
config.QPS = K8sClientConfigQPS
config.Burst = K8sClientConfigBurst
return config
}

View File

@@ -18,6 +18,8 @@ import (
"strings"
"time"
"github.com/argoproj/argo-cd/v2/util/io/files"
"github.com/Masterminds/semver/v3"
"github.com/TomOnTime/utfutil"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
@@ -810,7 +812,8 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string,
if directory = q.ApplicationSource.Directory; directory == nil {
directory = &v1alpha1.ApplicationSourceDirectory{}
}
targetObjs, err = findManifests(appPath, repoRoot, env, *directory, q.EnabledSourceTypes)
logCtx := log.WithField("application", q.AppName)
targetObjs, err = findManifests(logCtx, appPath, repoRoot, env, *directory, q.EnabledSourceTypes)
}
if err != nil {
return nil, err
@@ -1012,12 +1015,32 @@ func ksShow(appLabelKey, appPath string, ksonnetOpts *v1alpha1.ApplicationSource
var manifestFile = regexp.MustCompile(`^.*\.(yaml|yml|json|jsonnet)$`)
// findManifests looks at all yaml files in a directory and unmarshals them into a list of unstructured objects
func findManifests(appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory, enabledManifestGeneration map[string]bool) ([]*unstructured.Unstructured, error) {
func findManifests(logCtx *log.Entry, appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory, enabledManifestGeneration map[string]bool) ([]*unstructured.Unstructured, error) {
var objs []*unstructured.Unstructured
err := filepath.Walk(appPath, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(appPath, path)
if err != nil {
return fmt.Errorf("failed to get relative path of symlink: %w", err)
}
if files.IsSymlink(f) {
realPath, err := filepath.EvalSymlinks(path)
if err != nil {
logCtx.Debugf("error checking symlink realpath: %s", err)
if os.IsNotExist(err) {
log.Warnf("ignoring out-of-bounds symlink at %q: %s", relPath, err)
return nil
} else {
return fmt.Errorf("failed to evaluate symlink at %q: %w", relPath, err)
}
}
if !files.Inbound(realPath, appPath) {
logCtx.Warnf("illegal filepath in symlink: %s", realPath)
return fmt.Errorf("illegal filepath in symlink at %q", relPath)
}
}
if f.IsDir() {
if path != appPath && !directory.Recurse {
return filepath.SkipDir
@@ -1030,10 +1053,6 @@ func findManifests(appPath string, repoRoot string, env *v1alpha1.Env, directory
return nil
}
relPath, err := filepath.Rel(appPath, path)
if err != nil {
return err
}
if directory.Exclude != "" && glob.Match(directory.Exclude, relPath) {
return nil
}
@@ -1660,33 +1679,40 @@ func directoryPermissionInitializer(rootPath string) goio.Closer {
// nolint:unparam
func (s *Service) checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) (goio.Closer, error) {
closer := s.gitRepoInitializer(gitClient.Root())
return closer, checkoutRevision(gitClient, revision, submoduleEnabled)
}
func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) error {
err := gitClient.Init()
if err != nil {
return closer, status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
return status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
}
err = gitClient.Fetch(revision)
// Fetching with no revision first. Fetching with an explicit version can cause repo bloat. https://github.com/argoproj/argo-cd/issues/8845
err = gitClient.Fetch("")
if err != nil {
log.Infof("Failed to fetch revision %s: %v", revision, err)
log.Infof("Fallback to fetch default")
err = gitClient.Fetch("")
if err != nil {
return closer, status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
}
err = gitClient.Checkout(revision, submoduleEnabled)
if err != nil {
return closer, status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
}
return closer, err
return status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
}
err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
err = gitClient.Checkout(revision, submoduleEnabled)
if err != nil {
return closer, status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
// When fetching with no revision, only refs/heads/* and refs/remotes/origin/* are fetched. If checkout fails
// for the given revision, try explicitly fetching it.
log.Infof("Failed to checkout revision %s: %v", revision, err)
log.Infof("Fallback to fetching specific revision %s. ref might not have been in the default refspec fetched.", revision)
err = gitClient.Fetch(revision)
if err != nil {
return status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
}
err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
if err != nil {
return status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
}
}
return closer, err
return err
}
func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {

View File

@@ -16,6 +16,8 @@ import (
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -149,6 +151,76 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
assert.Equal(t, 3, len(res2.Manifests))
}
func Test_GenerateManifests_NoOutOfBoundsAccess(t *testing.T) {
testCases := []struct {
name string
outOfBoundsFilename string
outOfBoundsFileContents string
mustNotContain string // Optional string that must not appear in error or manifest output. If empty, use outOfBoundsFileContents.
}{
{
name: "out of bounds JSON file should not appear in error output",
outOfBoundsFilename: "test.json",
outOfBoundsFileContents: `{"some": "json"}`,
},
{
name: "malformed JSON file contents should not appear in error output",
outOfBoundsFilename: "test.json",
outOfBoundsFileContents: "$",
},
{
name: "out of bounds JSON manifest should not appear in manifest output",
outOfBoundsFilename: "test.json",
// JSON marshalling is deterministic. So if there's a leak, exactly this should appear in the manifests.
outOfBoundsFileContents: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
},
{
name: "out of bounds YAML manifest should not appear in manifest output",
outOfBoundsFilename: "test.yaml",
outOfBoundsFileContents: "apiVersion: v1\nkind: Secret\nmetadata:\n name: test\n namespace: default\ntype: Opaque",
mustNotContain: `{"apiVersion":"v1","kind":"Secret","metadata":{"name":"test","namespace":"default"},"type":"Opaque"}`,
},
}
for _, testCase := range testCases {
testCaseCopy := testCase
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
outOfBoundsDir := t.TempDir()
outOfBoundsFile := path.Join(outOfBoundsDir, testCaseCopy.outOfBoundsFilename)
err := os.WriteFile(outOfBoundsFile, []byte(testCaseCopy.outOfBoundsFileContents), os.FileMode(0444))
require.NoError(t, err)
repoDir := t.TempDir()
err = os.Symlink(outOfBoundsFile, path.Join(repoDir, testCaseCopy.outOfBoundsFilename))
require.NoError(t, err)
var mustNotContain = testCaseCopy.outOfBoundsFileContents
if testCaseCopy.mustNotContain != "" {
mustNotContain = testCaseCopy.mustNotContain
}
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}}
res, err := GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{})
require.Error(t, err)
assert.NotContains(t, err.Error(), mustNotContain)
assert.Contains(t, err.Error(), "illegal filepath")
assert.Nil(t, res)
})
}
}
func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) {
repoDir := t.TempDir()
err := os.Symlink("/obviously/does/not/exist", path.Join(repoDir, "test.yaml"))
require.NoError(t, err)
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{}}
_, err = GenerateManifests(context.Background(), repoDir, "", "", &q, false, &git.NoopCredsStore{})
require.NoError(t, err)
}
func TestGenerateManifests_K8SAPIResetCache(t *testing.T) {
service := newService("../..")
@@ -1641,7 +1713,7 @@ func TestFindResources(t *testing.T) {
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
Recurse: true,
Include: tc.include,
Exclude: tc.exclude,
@@ -1659,7 +1731,7 @@ func TestFindResources(t *testing.T) {
}
func TestFindManifests_Exclude(t *testing.T) {
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
Recurse: true,
Exclude: "subdir/deploymentSub.yaml",
}, map[string]bool{})
@@ -1672,7 +1744,7 @@ func TestFindManifests_Exclude(t *testing.T) {
}
func TestFindManifests_Exclude_NothingMatches(t *testing.T) {
objs, err := findManifests("testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
objs, err := findManifests(&log.Entry{}, "testdata/app-include-exclude", ".", nil, argoappv1.ApplicationSourceDirectory{
Recurse: true,
Exclude: "nothing.yaml",
}, map[string]bool{})
@@ -1816,3 +1888,51 @@ func TestInit(t *testing.T) {
require.Error(t, err)
require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2"))
}
// TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In
// other words, we haven't regressed and caused this issue again: https://github.com/argoproj/argo-cd/issues/4935
func TestCheckoutRevisionCanGetNonstandardRefs(t *testing.T) {
rootPath, err := ioutil.TempDir("", "")
require.NoError(t, err)
sourceRepoPath, err := ioutil.TempDir(rootPath, "")
require.NoError(t, err)
// Create a repo such that one commit is on a non-standard ref _and nowhere else_. This is meant to simulate, for
// example, a GitHub ref for a pull into one repo from a fork of that repo.
runGit(t, sourceRepoPath, "init")
runGit(t, sourceRepoPath, "checkout", "-b", "main") // make sure there's a main branch to switch back to
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
runGit(t, sourceRepoPath, "checkout", "-b", "branch")
runGit(t, sourceRepoPath, "commit", "-m", "empty", "--allow-empty")
sha := runGit(t, sourceRepoPath, "rev-parse", "HEAD")
runGit(t, sourceRepoPath, "update-ref", "refs/pull/123/head", strings.TrimSuffix(sha, "\n"))
runGit(t, sourceRepoPath, "checkout", "main")
runGit(t, sourceRepoPath, "branch", "-D", "branch")
destRepoPath, err := ioutil.TempDir(rootPath, "")
require.NoError(t, err)
gitClient, err := git.NewClientExt("file://"+sourceRepoPath, destRepoPath, &git.NopCreds{}, true, false, "")
require.NoError(t, err)
pullSha, err := gitClient.LsRemote("refs/pull/123/head")
require.NoError(t, err)
err = checkoutRevision(gitClient, "does-not-exist", false)
assert.Error(t, err)
err = checkoutRevision(gitClient, pullSha, false)
assert.NoError(t, err)
}
// runGit runs a git command in the given working directory. If the command succeeds, it returns the combined standard
// and error output. If it fails, it stops the test with a failure message.
func runGit(t *testing.T, workDir string, args ...string) string {
cmd := exec.Command("git", args...)
cmd.Dir = workDir
out, err := cmd.CombinedOutput()
stringOut := string(out)
require.NoError(t, err, stringOut)
return stringOut
}

View File

@@ -488,6 +488,21 @@ func (s *Server) ListResourceEvents(ctx context.Context, q *application.Applicat
"involvedObject.namespace": a.Namespace,
}).String()
} else {
tree, err := s.getAppResources(ctx, a)
if err != nil {
return nil, err
}
found := false
for _, n := range append(tree.Nodes, tree.OrphanedNodes...) {
if n.ResourceRef.UID == q.ResourceUID && n.ResourceRef.Name == q.ResourceName && n.ResourceRef.Namespace == q.ResourceNamespace {
found = true
break
}
}
if !found {
return nil, status.Errorf(codes.InvalidArgument, "%s not found as part of application %s", q.ResourceName, *q.Name)
}
namespace = q.ResourceNamespace
var config *rest.Config
config, err = s.getApplicationClusterConfig(ctx, a)
@@ -937,7 +952,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap
return &tree, err
}
func (s *Server) getAppResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) {
func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) {
a, err := s.appLister.Get(*q.Name)
if err != nil {
return nil, nil, nil, err
@@ -952,7 +967,7 @@ func (s *Server) getAppResource(ctx context.Context, action string, q *applicati
}
found := tree.FindNode(q.Group, q.Kind, q.Namespace, q.ResourceName)
if found == nil {
if found == nil || found.ResourceRef.UID == "" {
return nil, nil, nil, status.Errorf(codes.InvalidArgument, "%s %s %s not found as part of application %s", q.Kind, q.Group, q.ResourceName, *q.Name)
}
config, err := s.getApplicationClusterConfig(ctx, a)
@@ -963,7 +978,7 @@ func (s *Server) getAppResource(ctx context.Context, action string, q *applicati
}
func (s *Server) GetResource(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ApplicationResourceResponse, error) {
res, config, _, err := s.getAppResource(ctx, rbacpolicy.ActionGet, q)
res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q)
if err != nil {
return nil, err
}
@@ -1008,7 +1023,7 @@ func (s *Server) PatchResource(ctx context.Context, q *application.ApplicationRe
Version: q.Version,
Group: q.Group,
}
res, config, a, err := s.getAppResource(ctx, rbacpolicy.ActionUpdate, resourceRequest)
res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionUpdate, resourceRequest)
if err != nil {
return nil, err
}
@@ -1048,7 +1063,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR
Version: q.Version,
Group: q.Group,
}
res, config, a, err := s.getAppResource(ctx, rbacpolicy.ActionDelete, resourceRequest)
res, config, a, err := s.getAppLiveResource(ctx, rbacpolicy.ActionDelete, resourceRequest)
if err != nil {
return nil, err
}
@@ -1319,7 +1334,7 @@ func getSelectedPods(treeNodes []appv1.ResourceNode, q *application.ApplicationP
var pods []appv1.ResourceNode
isTheOneMap := make(map[string]bool)
for _, treeNode := range treeNodes {
if treeNode.Kind == kube.PodKind && treeNode.Group == "" {
if treeNode.Kind == kube.PodKind && treeNode.Group == "" && treeNode.UID != "" {
if isTheSelectedOne(&treeNode, q, treeNodes, isTheOneMap) {
pods = append(pods, treeNode)
}
@@ -1609,7 +1624,7 @@ func (s *Server) logResourceEvent(res *appv1.ResourceNode, ctx context.Context,
}
func (s *Server) ListResourceActions(ctx context.Context, q *application.ApplicationResourceRequest) (*application.ResourceActionsListResponse, error) {
res, config, _, err := s.getAppResource(ctx, rbacpolicy.ActionGet, q)
res, config, _, err := s.getAppLiveResource(ctx, rbacpolicy.ActionGet, q)
if err != nil {
return nil, err
}
@@ -1660,7 +1675,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA
Group: q.Group,
}
actionRequest := fmt.Sprintf("%s/%s/%s/%s", rbacpolicy.ActionAction, q.Group, q.Kind, q.Action)
res, config, a, err := s.getAppResource(ctx, actionRequest, resourceRequest)
res, config, a, err := s.getAppLiveResource(ctx, actionRequest, resourceRequest)
if err != nil {
return nil, err
}

View File

@@ -951,6 +951,8 @@ func (a *ArgoCDServer) Authenticate(ctx context.Context) (context.Context, error
}
if !argoCDSettings.AnonymousUserEnabled {
return ctx, claimsErr
} else {
ctx = context.WithValue(ctx, "claims", "")
}
}

View File

@@ -3,6 +3,8 @@ package server
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
@@ -12,6 +14,7 @@ import (
"github.com/golang-jwt/jwt/v4"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
@@ -432,6 +435,386 @@ func TestAuthenticate(t *testing.T) {
}
}
func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.RequestURI {
case "/api/dex/.well-known/openid-configuration":
_, err := io.WriteString(w, fmt.Sprintf(`
{
"issuer": "%[1]s/api/dex",
"authorization_endpoint": "%[1]s/api/dex/auth",
"token_endpoint": "%[1]s/api/dex/token",
"jwks_uri": "%[1]s/api/dex/keys",
"userinfo_endpoint": "%[1]s/api/dex/userinfo",
"device_authorization_endpoint": "%[1]s/api/dex/device/code",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256", "HS256"
],
"code_challenge_methods_supported": [
"S256",
"plain"
],
"scopes_supported": [
"openid",
"email",
"groups",
"profile",
"offline_access"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
],
"claims_supported": [
"iss",
"sub",
"aud",
"iat",
"exp",
"email",
"email_verified",
"locale",
"name",
"preferred_username",
"at_hash"
]
}`, url))
if err != nil {
t.Fail()
}
default:
w.WriteHeader(404)
}
}
}
func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool) (argocd *ArgoCDServer, dexURL string) {
cm := test.NewFakeConfigMap()
if anonymousEnabled {
cm.Data["users.anonymous.enabled"] = "true"
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return // Start with a placeholder. We need the server URL before setting up the real handler.
}))
ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
dexMockHandler(t, ts.URL)(w, r)
})
if withFakeSSO {
cm.Data["url"] = ts.URL
cm.Data["dex.config"] = `
connectors:
# OIDC
- type: OIDC
id: oidc
name: OIDC
config:
issuer: https://auth.example.gom
clientID: test-client
clientSecret: $dex.oidc.clientSecret`
}
secret := test.NewFakeSecret()
kubeclientset := fake.NewSimpleClientset(cm, secret)
appClientSet := apps.NewSimpleClientset()
argoCDOpts := ArgoCDServerOpts{
Namespace: test.FakeArgoCDNamespace,
KubeClientset: kubeclientset,
AppClientset: appClientSet,
}
if withFakeSSO {
argoCDOpts.DexServerAddr = ts.URL
}
argocd = NewServer(context.Background(), argoCDOpts)
return argocd, ts.URL
}
func TestAuthenticate_3rd_party_JWTs(t *testing.T) {
type testData struct {
test string
anonymousEnabled bool
claims jwt.RegisteredClaims
expectedErrorContains string
expectedClaims interface{}
}
var tests = []testData{
{
test: "anonymous disabled, no audience",
anonymousEnabled: false,
claims: jwt.RegisteredClaims{},
expectedErrorContains: "no audience found in the token",
expectedClaims: nil,
},
{
test: "anonymous enabled, no audience",
anonymousEnabled: true,
claims: jwt.RegisteredClaims{},
expectedErrorContains: "",
expectedClaims: "",
},
{
test: "anonymous disabled, unexpired token, admin claim",
anonymousEnabled: false,
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
expectedErrorContains: "id token signed with unsupported algorithm",
expectedClaims: nil,
},
{
test: "anonymous enabled, unexpired token, admin claim",
anonymousEnabled: true,
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))},
expectedErrorContains: "",
expectedClaims: "",
},
{
test: "anonymous disabled, expired token, admin claim",
anonymousEnabled: false,
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
expectedErrorContains: "token is expired",
expectedClaims: jwt.RegisteredClaims{Issuer:"sso"},
},
{
test: "anonymous enabled, expired token, admin claim",
anonymousEnabled: true,
claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())},
expectedErrorContains: "",
expectedClaims: "",
},
}
for _, testData := range tests {
testDataCopy := testData
t.Run(testDataCopy.test, func(t *testing.T) {
t.Parallel()
argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, true)
ctx := context.Background()
testDataCopy.claims.Issuer = fmt.Sprintf("%s/api/dex", dexURL)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, testDataCopy.claims)
tokenString, err := token.SignedString([]byte("key"))
require.NoError(t, err)
ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))
ctx, err = argocd.Authenticate(ctx)
claims := ctx.Value("claims")
if testDataCopy.expectedClaims == nil {
assert.Nil(t, claims)
} else {
assert.Equal(t, testDataCopy.expectedClaims, claims)
}
if testDataCopy.expectedErrorContains != "" {
assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthenticate_no_request_metadata(t *testing.T) {
type testData struct {
test string
anonymousEnabled bool
expectedErrorContains string
expectedClaims interface{}
}
var tests = []testData{
{
test: "anonymous disabled",
anonymousEnabled: false,
expectedErrorContains: "no session information",
expectedClaims: nil,
},
{
test: "anonymous enabled",
anonymousEnabled: true,
expectedErrorContains: "",
expectedClaims: "",
},
}
for _, testData := range tests {
testDataCopy := testData
t.Run(testDataCopy.test, func(t *testing.T) {
t.Parallel()
argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true)
ctx := context.Background()
ctx, err := argocd.Authenticate(ctx)
claims := ctx.Value("claims")
assert.Equal(t, testDataCopy.expectedClaims, claims)
if testDataCopy.expectedErrorContains != "" {
assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request")
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthenticate_no_SSO(t *testing.T) {
type testData struct {
test string
anonymousEnabled bool
expectedErrorMessage string
expectedClaims interface{}
}
var tests = []testData{
{
test: "anonymous disabled",
anonymousEnabled: false,
expectedErrorMessage: "SSO is not configured",
expectedClaims: nil,
},
{
test: "anonymous enabled",
anonymousEnabled: true,
expectedErrorMessage: "",
expectedClaims: "",
},
}
for _, testData := range tests {
testDataCopy := testData
t.Run(testDataCopy.test, func(t *testing.T) {
t.Parallel()
argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false)
ctx := context.Background()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{Issuer: fmt.Sprintf("%s/api/dex", dexURL)})
tokenString, err := token.SignedString([]byte("key"))
require.NoError(t, err)
ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString))
ctx, err = argocd.Authenticate(ctx)
claims := ctx.Value("claims")
assert.Equal(t, testDataCopy.expectedClaims, claims)
if testDataCopy.expectedErrorMessage != "" {
assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
} else {
assert.NoError(t, err)
}
})
}
}
func TestAuthenticate_bad_request_metadata(t *testing.T) {
type testData struct {
test string
anonymousEnabled bool
metadata metadata.MD
expectedErrorMessage string
expectedClaims interface{}
}
var tests = []testData{
{
test: "anonymous disabled, empty metadata",
anonymousEnabled: false,
metadata: metadata.MD{},
expectedErrorMessage: "no session information",
expectedClaims: nil,
},
{
test: "anonymous enabled, empty metadata",
anonymousEnabled: true,
metadata: metadata.MD{},
expectedErrorMessage: "",
expectedClaims: "",
},
{
test: "anonymous disabled, empty tokens",
anonymousEnabled: false,
metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}},
expectedErrorMessage: "no session information",
expectedClaims: nil,
},
{
test: "anonymous enabled, empty tokens",
anonymousEnabled: true,
metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}},
expectedErrorMessage: "",
expectedClaims: "",
},
{
test: "anonymous disabled, bad tokens",
anonymousEnabled: false,
metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
expectedErrorMessage: "token contains an invalid number of segments",
expectedClaims: nil,
},
{
test: "anonymous enabled, bad tokens",
anonymousEnabled: true,
metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"),
expectedErrorMessage: "",
expectedClaims: "",
},
{
test: "anonymous disabled, bad auth header",
anonymousEnabled: false,
metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
expectedErrorMessage: "no audience found in the token",
expectedClaims: nil,
},
{
test: "anonymous enabled, bad auth header",
anonymousEnabled: true,
metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
expectedErrorMessage: "",
expectedClaims: "",
},
{
test: "anonymous disabled, bad auth cookie",
anonymousEnabled: false,
metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
expectedErrorMessage: "no audience found in the token",
expectedClaims: nil,
},
{
test: "anonymous enabled, bad auth cookie",
anonymousEnabled: true,
metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}},
expectedErrorMessage: "",
expectedClaims: "",
},
}
for _, testData := range tests {
testDataCopy := testData
t.Run(testDataCopy.test, func(t *testing.T) {
t.Parallel()
argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true)
ctx := context.Background()
ctx = metadata.NewIncomingContext(context.Background(), testDataCopy.metadata)
ctx, err := argocd.Authenticate(ctx)
claims := ctx.Value("claims")
assert.Equal(t, testDataCopy.expectedClaims, claims)
if testDataCopy.expectedErrorMessage != "" {
assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request")
} else {
assert.NoError(t, err)
}
})
}
}
func Test_getToken(t *testing.T) {
token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
t.Run("Empty", func(t *testing.T) {

View File

@@ -2,7 +2,7 @@ FROM redis:6.2.6 as redis
FROM node:12.18.4 as node
FROM golang:1.17.6 as golang
FROM golang:1.17 as golang
FROM registry:2.7.1 as registry

View File

@@ -909,64 +909,125 @@ func TestSyncAsync(t *testing.T) {
Expect(SyncStatusIs(SyncStatusCodeSynced))
}
func TestPermissions(t *testing.T) {
EnsureCleanState(t)
appName := Name()
_, err := RunCli("proj", "create", "test")
assert.NoError(t, err)
// make sure app cannot be created without permissions in project
_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
assert.Error(t, err)
sourceError := fmt.Sprintf("application repo %s is not permitted in project 'test'", RepoURL(RepoURLTypeFile))
destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'test'", KubernetesInternalAPIServerAddr, DeploymentNamespace())
assert.Contains(t, err.Error(), sourceError)
assert.Contains(t, err.Error(), destinationError)
proj, err := AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Get(context.Background(), "test", metav1.GetOptions{})
assert.NoError(t, err)
proj.Spec.Destinations = []ApplicationDestination{{Server: "*", Namespace: "*"}}
proj.Spec.SourceRepos = []string{"*"}
proj, err = AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Update(context.Background(), proj, metav1.UpdateOptions{})
assert.NoError(t, err)
// make sure controller report permissions issues in conditions
_, err = RunCli("app", "create", appName, "--repo", RepoURL(RepoURLTypeFile),
"--path", guestbookPath, "--project", "test", "--dest-server", KubernetesInternalAPIServerAddr, "--dest-namespace", DeploymentNamespace())
assert.NoError(t, err)
defer func() {
err = AppClientset.ArgoprojV1alpha1().Applications(ArgoCDNamespace).Delete(context.Background(), appName, metav1.DeleteOptions{})
assert.NoError(t, err)
}()
proj.Spec.Destinations = []ApplicationDestination{}
proj.Spec.SourceRepos = []string{}
_, err = AppClientset.ArgoprojV1alpha1().AppProjects(ArgoCDNamespace).Update(context.Background(), proj, metav1.UpdateOptions{})
assert.NoError(t, err)
time.Sleep(1 * time.Second)
closer, client, err := ArgoCDClientset.NewApplicationClient()
assert.NoError(t, err)
defer io.Close(closer)
refresh := string(RefreshTypeNormal)
app, err := client.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName, Refresh: &refresh})
assert.NoError(t, err)
destinationErrorExist := false
sourceErrorExist := false
for i := range app.Status.Conditions {
if strings.Contains(app.Status.Conditions[i].Message, destinationError) {
destinationErrorExist = true
}
if strings.Contains(app.Status.Conditions[i].Message, sourceError) {
sourceErrorExist = true
// assertResourceActions verifies if view/modify resource actions are successful/failing for given application
func assertResourceActions(t *testing.T, appName string, successful bool) {
assertError := func(err error, message string) {
if successful {
assert.NoError(t, err)
} else {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), message)
}
}
}
assert.True(t, destinationErrorExist)
assert.True(t, sourceErrorExist)
closer, cdClient := ArgoCDClientset.NewApplicationClientOrDie()
defer io.Close(closer)
deploymentResource, err := KubeClientset.AppsV1().Deployments(DeploymentNamespace()).Get(context.Background(), "guestbook-ui", metav1.GetOptions{})
require.NoError(t, err)
logs, err := cdClient.PodLogs(context.Background(), &applicationpkg.ApplicationPodLogsQuery{
Group: pointer.String("apps"), Kind: pointer.String("Deployment"), Name: &appName, Namespace: DeploymentNamespace(),
})
require.NoError(t, err)
_, err = logs.Recv()
assertError(err, "EOF")
expectedError := fmt.Sprintf("Deployment apps guestbook-ui not found as part of application %s", appName)
_, err = cdClient.ListResourceEvents(context.Background(), &applicationpkg.ApplicationResourceEventsQuery{
Name: &appName, ResourceName: "guestbook-ui", ResourceNamespace: DeploymentNamespace(), ResourceUID: string(deploymentResource.UID)})
assertError(err, fmt.Sprintf("%s not found as part of application %s", "guestbook-ui", appName))
_, err = cdClient.GetResource(context.Background(), &applicationpkg.ApplicationResourceRequest{
Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment"})
assertError(err, expectedError)
_, err = cdClient.DeleteResource(context.Background(), &applicationpkg.ApplicationResourceDeleteRequest{
Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment",
})
assertError(err, expectedError)
_, err = cdClient.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{
Name: &appName, ResourceName: "guestbook-ui", Namespace: DeploymentNamespace(), Version: "v1", Group: "apps", Kind: "Deployment", Action: "restart",
})
assertError(err, expectedError)
}
func TestPermissions(t *testing.T) {
appCtx := Given(t)
projName := "argo-project"
projActions := projectFixture.
Given(t).
Name(projName).
When().
Create()
sourceError := fmt.Sprintf("application repo %s is not permitted in project 'argo-project'", RepoURL(RepoURLTypeFile))
destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'argo-project'", KubernetesInternalAPIServerAddr, DeploymentNamespace())
appCtx.
Path("guestbook-logs").
Project(projName).
When().
IgnoreErrors().
// ensure app is not created if project permissions are missing
CreateApp().
Then().
Expect(Error("", sourceError)).
Expect(Error("", destinationError)).
When().
DoNotIgnoreErrors().
// add missing permissions, create and sync app
And(func() {
projActions.AddDestination("*", "*")
projActions.AddSource("*")
}).
CreateApp().
Sync().
Then().
// make sure application resource actiions are successful
And(func(app *Application) {
assertResourceActions(t, app.Name, true)
}).
When().
// remove projet permissions and "refresh" app
And(func() {
projActions.UpdateProject(func(proj *AppProject) {
proj.Spec.Destinations = nil
proj.Spec.SourceRepos = nil
})
}).
Refresh(RefreshTypeNormal).
Then().
// ensure app resource tree is empty when source/destination permissions are missing
Expect(Condition(ApplicationConditionInvalidSpecError, destinationError)).
Expect(Condition(ApplicationConditionInvalidSpecError, sourceError)).
And(func(app *Application) {
closer, cdClient := ArgoCDClientset.NewApplicationClientOrDie()
defer io.Close(closer)
tree, err := cdClient.ResourceTree(context.Background(), &applicationpkg.ResourcesQuery{ApplicationName: &app.Name})
require.NoError(t, err)
assert.Len(t, tree.Nodes, 0)
assert.Len(t, tree.OrphanedNodes, 0)
}).
When().
// add missing permissions but deny management of Deployment kind
And(func() {
projActions.
AddDestination("*", "*").
AddSource("*").
UpdateProject(func(proj *AppProject) {
proj.Spec.NamespaceResourceBlacklist = []metav1.GroupKind{{Group: "*", Kind: "Deployment"}}
})
}).
Refresh(RefreshTypeNormal).
Then().
// make sure application resource actiions are failing
And(func(app *Application) {
assertResourceActions(t, "test-permissions", false)
})
}
func TestPermissionWithScopedRepo(t *testing.T) {

View File

@@ -1,6 +1,12 @@
package project
import (
"context"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/test/e2e/fixture"
)
@@ -34,6 +40,25 @@ func (a *Actions) Create(args ...string) *Actions {
return a
}
func (a *Actions) AddDestination(cluster string, namespace string) *Actions {
a.runCli("proj", "add-destination", a.context.name, cluster, namespace)
return a
}
func (a *Actions) AddSource(repo string) *Actions {
a.runCli("proj", "add-source", a.context.name, repo)
return a
}
func (a *Actions) UpdateProject(updater func(project *v1alpha1.AppProject)) *Actions {
proj, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Get(context.TODO(), a.context.name, v1.GetOptions{})
require.NoError(a.context.t, err)
updater(proj)
_, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Update(context.TODO(), proj, v1.UpdateOptions{})
require.NoError(a.context.t, err)
return a
}
func (a *Actions) Name(name string) *Actions {
a.context.name = name
return a
@@ -72,4 +97,7 @@ func (a *Actions) Then() *Consequences {
func (a *Actions) runCli(args ...string) {
a.context.t.Helper()
a.lastOutput, a.lastError = fixture.RunCli(args...)
if !a.ignoreErrors {
require.Empty(a.context.t, a.lastError)
}
}

View File

@@ -1,4 +1,4 @@
FROM golang:1.17.6 AS go
FROM golang:1.17 AS go
RUN go install github.com/mattn/goreman@latest && \
go install github.com/kisielk/godepgraph@latest

View File

@@ -20,7 +20,7 @@ interface State {
loginError: string;
loginInProgress: boolean;
returnUrl: string;
ssoLoginError: string;
hasSsoLoginError: boolean;
}
export class Login extends React.Component<RouteComponentProps<{}>, State> {
@@ -31,13 +31,13 @@ export class Login extends React.Component<RouteComponentProps<{}>, State> {
public static getDerivedStateFromProps(props: RouteComponentProps<{}>): Partial<State> {
const search = new URLSearchParams(props.history.location.search);
const returnUrl = search.get('return_url') || '';
const ssoLoginError = search.get('sso_error') || '';
return {ssoLoginError, returnUrl};
const hasSsoLoginError = search.get('has_sso_error') === 'true';
return {hasSsoLoginError, returnUrl};
}
constructor(props: RouteComponentProps<{}>) {
super(props);
this.state = {authSettings: null, loginError: null, returnUrl: null, ssoLoginError: null, loginInProgress: false};
this.state = {authSettings: null, loginError: null, returnUrl: null, hasSsoLoginError: false, loginInProgress: false};
}
public async componentDidMount() {
@@ -69,7 +69,7 @@ export class Login extends React.Component<RouteComponentProps<{}>, State> {
)}
</button>
</a>
{this.state.ssoLoginError && <div className='argo-form-row__error-msg'>{this.state.ssoLoginError}</div>}
{this.state.hasSsoLoginError && <div className='argo-form-row__error-msg'>Login failed.</div>}
{authSettings && !authSettings.userLoginsDisabled && (
<div className='login__saml-separator'>
<span>or</span>

View File

@@ -88,6 +88,10 @@ const config = {
{
from: 'node_modules/redoc/bundles/redoc.standalone.js',
to: 'assets/scripts/redoc.standalone.js'
},
{
from: 'node_modules/monaco-editor/min/vs/base/browser/ui/codicons/codicon',
to: 'assets/fonts'
}
]
}),

View File

@@ -1,3 +1,8 @@
@font-face {
font-family: "codicon";
src: url("./fonts/codicon.ttf") format("truetype");
}
/* === Heebo - 300 */
@font-face {
font-family: 'Heebo';

View File

@@ -3,20 +3,18 @@ package dex
import (
"bytes"
"fmt"
"html"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"path"
"regexp"
"strconv"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/util/errors"
)
var messageRe = regexp.MustCompile(`<p>(.*)([\s\S]*?)<\/p>`)
func decorateDirector(director func(req *http.Request), target *url.URL) func(req *http.Request) {
return func(req *http.Request) {
director(req)
@@ -44,16 +42,10 @@ func NewDexHTTPReverseProxy(serverAddr string, baseHRef string) func(writer http
if err != nil {
return err
}
var message string
matches := messageRe.FindSubmatch(b)
if len(matches) > 1 {
message = html.UnescapeString(string(matches[1]))
} else {
message = "Unknown error"
}
log.Errorf("received error from dex: %s", string(b))
resp.ContentLength = 0
resp.Header.Set("Content-Length", strconv.Itoa(0))
resp.Header.Set("Location", fmt.Sprintf("%s?sso_error=%s", path.Join(baseHRef, "login"), url.QueryEscape(message)))
resp.Header.Set("Location", fmt.Sprintf("%s?has_sso_error=true", path.Join(baseHRef, "login")))
resp.StatusCode = http.StatusSeeOther
resp.Body = ioutil.NopCloser(bytes.NewReader(make([]byte, 0)))
return nil

View File

@@ -408,7 +408,7 @@ func Test_DexReverseProxy(t *testing.T) {
assert.Equal(t, http.StatusSeeOther, resp.StatusCode)
location, _ := resp.Location()
fmt.Printf("%s %s\n", resp.Status, location.RequestURI())
assert.True(t, strings.HasPrefix(location.RequestURI(), "/login?sso_error"))
assert.True(t, strings.HasPrefix(location.RequestURI(), "/login?has_sso_error=true"))
})
t.Run("Invalid URL for Dex reverse proxy", func(t *testing.T) {

35
util/io/files/util.go Normal file
View File

@@ -0,0 +1,35 @@
package files
import (
"io/fs"
"os"
"path/filepath"
"strings"
)
// Inbound will validate if the given candidate path is inside the
// baseDir. This is useful to make sure that malicious candidates
// are not targeting a file outside of baseDir boundaries.
// Considerations:
// - baseDir must be absolute path. Will return false otherwise
// - candidate can be absolute or relative path
// - candidate should not be symlink as only syntatic validation is
// applied by this function
func Inbound(candidate, baseDir string) bool {
if !filepath.IsAbs(baseDir) {
return false
}
var target string
if filepath.IsAbs(candidate) {
target = filepath.Clean(candidate)
} else {
target = filepath.Join(baseDir, candidate)
}
return strings.HasPrefix(target, filepath.Clean(baseDir)+string(os.PathSeparator))
}
// IsSymlink return true if the given FileInfo relates to a
// symlink file. Returns false otherwise.
func IsSymlink(fi os.FileInfo) bool {
return fi.Mode()&fs.ModeSymlink == fs.ModeSymlink
}

View File

@@ -0,0 +1,63 @@
package files_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/v2/util/io/files"
)
func TestInbound(t *testing.T) {
type testcase struct {
name string
candidate string
basedir string
expected bool
}
cases := []testcase{
{
name: "will return true if candidate is inbound",
candidate: "/home/test/app/readme.md",
basedir: "/home/test",
expected: true,
},
{
name: "will return false if candidate is not inbound",
candidate: "/home/test/../readme.md",
basedir: "/home/test",
expected: false,
},
{
name: "will return true if candidate is relative inbound",
candidate: "./readme.md",
basedir: "/home/test",
expected: true,
},
{
name: "will return false if candidate is relative outbound",
candidate: "../readme.md",
basedir: "/home/test",
expected: false,
},
{
name: "will return false if basedir is relative",
candidate: "/home/test/app/readme.md",
basedir: "./test",
expected: false,
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
// given
t.Parallel()
// when
inbound := files.Inbound(c.candidate, c.basedir)
// then
assert.Equal(t, c.expected, inbound)
})
}
}

View File

@@ -210,7 +210,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
token, err := jwt.ParseWithClaims(tokenString, &claims, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return argoCDSettings.ServerSignature, nil
})
@@ -262,7 +262,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
}
if account.PasswordMtime != nil && issuedAt.Before(*account.PasswordMtime) {
return nil, "", fmt.Errorf("Account password has changed since token issued")
return nil, "", fmt.Errorf("account password has changed since token issued")
}
newToken := ""
@@ -477,7 +477,7 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, string,
// IDP signed token
prov, err := mgr.provider()
if err != nil {
return claims, "", err
return nil, "", err
}
// Token must be verified for at least one audience
@@ -489,16 +489,30 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, string,
break
}
}
// The token verification has failed. If the token has expired, we will
// return a dummy claims only containing a value for the issuer, so the
// UI can handle expired tokens appropriately.
if err != nil {
return claims, "", err
if strings.HasPrefix(err.Error(), "oidc: token is expired") {
claims = jwt.RegisteredClaims{
Issuer: "sso",
}
return claims, "", err
}
return nil, "", err
}
if idToken == nil {
return claims, "", fmt.Errorf("No audience found in the token")
return nil, "", fmt.Errorf("no audience found in the token")
}
var claims jwt.MapClaims
err = idToken.Claims(&claims)
return claims, "", err
if err != nil {
return nil, "", err
}
return claims, "", nil
}
}