mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-26 12:38:47 +01:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8b7df946 | ||
|
|
5d515b8423 | ||
|
|
69dcee049e | ||
|
|
d36d95dc9f | ||
|
|
df79d7db1d | ||
|
|
7165431a84 | ||
|
|
13ec3f43d8 | ||
|
|
eea93c5103 | ||
|
|
2cc81959b4 | ||
|
|
07ac038a8f | ||
|
|
3828da7f8c | ||
|
|
3bd9121545 | ||
|
|
df6e7c169e | ||
|
|
25b01666f0 | ||
|
|
9d6e6d84de | ||
|
|
7f9ff6e8c3 | ||
|
|
ecc2af9dca | ||
|
|
c5b0279050 | ||
|
|
6df17e7c56 | ||
|
|
e55ecf9107 | ||
|
|
21f208f17e |
19
.github/workflows/ci-build.yaml
vendored
19
.github/workflows/ci-build.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/image.yaml
vendored
2
.github/workflows/image.yaml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
types: [ labeled, unlabeled, opened, synchronize, reopened ]
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
GOLANG_VERSION: '1.17'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
- '!release-v0*'
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
GOLANG_VERSION: '1.17'
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
8
controller/cache/cache.go
vendored
8
controller/cache/cache.go
vendored
@@ -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
|
||||
}
|
||||
|
||||
4
controller/cache/mocks/LiveStateCache.go
vendored
4
controller/cache/mocks/LiveStateCache.go
vendored
@@ -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)
|
||||
|
||||
@@ -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
|
||||
113
docs/roadmap.md
113
docs/roadmap.md
@@ -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,
|
||||
|
||||
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -11,4 +11,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.3.1
|
||||
newTag: v2.3.4
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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", "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("./fonts/codicon.ttf") format("truetype");
|
||||
}
|
||||
|
||||
/* === Heebo - 300 */
|
||||
@font-face {
|
||||
font-family: 'Heebo';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
35
util/io/files/util.go
Normal 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
|
||||
}
|
||||
63
util/io/files/util_test.go
Normal file
63
util/io/files/util_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user