Compare commits

...

21 Commits

Author SHA1 Message Date
argo-bot
ed734fedbb Bump version to 2.3.0-rc4 2022-02-03 21:46:08 +00:00
argo-bot
7414f2d42c Bump version to 2.3.0-rc4 2022-02-03 21:45:50 +00:00
jannfis
8139df8983 Merge pull request from GHSA-63qx-x74g-jcr7
Signed-off-by: jannfis <jann@mistrust.net>
2022-02-03 20:37:46 +01:00
pasha-codefresh
f364330de2 fix: fix example in project scoped repositories (#8357)
fix: fix example in project scoped repositories (#8357)

Signed-off-by: pashavictorovich <pavel@codefresh.io>
2022-02-03 09:45:43 -08:00
pasha-codefresh
4ec67d8b08 fix: applications page is crashing if nothing marked as favorites (#8356)
fix: applications page is crashing if nothing marked as favorites (#8356)

Signed-off-by: pashavictorovich <pavel@codefresh.io>
2022-02-03 09:45:39 -08:00
Leonardo Luz Almeida
3d3f81df4a chore: update actions/setup-go to v2 (#8349)
Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
2022-02-03 09:45:34 -08:00
argo-bot
37f01f6f32 Bump version to 2.3.0-rc2 2022-02-02 22:37:02 +00:00
argo-bot
36eab6b82c Bump version to 2.3.0-rc2 2022-02-02 22:36:47 +00:00
Leonardo Luz Almeida
793acc147f chore: Use go install to add spdx-sbom-generator (#8346)
Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
2022-02-02 13:51:14 -08:00
Leonardo Luz Almeida
b50609c0e6 chore: generate sbom for the released docker image (#8338)
Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
2022-02-02 13:51:10 -08:00
Michael Crenshaw
5f48ce96c6 chore: use go install instead of deprecated go get (#8333)
* chore: use go install instead of deprecated go get

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

* docs: readme fixes

Signed-off-by: Michael Crenshaw <michael@crenshaw.dev>
2022-02-02 13:30:43 -08:00
Alexander Matyushentsev
c32460a2bc chore: automate bundling argocd addons during release process (#8336)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-02-02 13:28:40 -08:00
Ben Ye
7eb1aba99b fix: register controller workqueue metrics correctly (#8318)
Signed-off-by: Ben Ye <ben.ye@bytedance.com>
2022-02-01 12:02:55 -08:00
Alexander Matyushentsev
1a8139f4d6 fix: make sure release workflow publish image with "v" in front of version (#8335)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-02-01 10:27:40 -08:00
Leonardo Luz Almeida
02c03c3b26 chore: generate and upload sbom during release (#8332)
Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
2022-02-01 10:27:34 -08:00
Alexander Matyushentsev
cdb20d5060 docs: mention argocd notifications and applicationset changes in upgrade instructions (#8312)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-02-01 10:27:27 -08:00
argo-bot
7d7eed4932 Bump version to 2.3.0-rc1 2022-01-30 21:42:54 +00:00
argo-bot
af8c5eb07a Bump version to 2.3.0-rc1 2022-01-30 21:42:41 +00:00
Alexander Matyushentsev
1a476f7564 fix: argocd build fails on windows (#8319)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-01-30 13:02:01 -08:00
Saumeya Katyal
7f15389c72 feat: favourite ui feature (#8210)
* feat: favourite ui feature (#8210)

Signed-off-by: saumeya <saumeyakatyal@gmail.com>
2022-01-28 12:55:23 -08:00
Alexander Matyushentsev
1a3556e1cc fix: add missing steps in release workflow to setup docker buildx (#8311)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2022-01-28 12:04:34 -08:00
44 changed files with 44084 additions and 599 deletions

View File

@@ -379,7 +379,7 @@ jobs:
- name: Download Go dependencies
run: |
go mod download
go get github.com/mattn/goreman
go install github.com/mattn/goreman@latest
- name: Install all tools required for building & testing
run: |
make install-test-tools-local

View File

@@ -95,7 +95,7 @@ jobs:
echo "=========== BEGIN COMMIT MESSAGE ============="
git show ${SOURCE_TAG}
echo "============ END COMMIT MESSAGE =============="
# Quite dirty hack to get the release notes from the annotated tag
# into a temporary file.
RELEASE_NOTES=$(mktemp -p /tmp release-notes.XXXXXX)
@@ -142,7 +142,7 @@ jobs:
echo "RELEASE_NOTES=${RELEASE_NOTES}" >> $GITHUB_ENV
- name: Setup Golang
uses: actions/setup-go@v1
uses: actions/setup-go@v2
with:
go-version: ${{ env.GOLANG_VERSION }}
@@ -197,12 +197,14 @@ jobs:
if: ${{ env.DRY_RUN != 'true' }}
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- name: Build and push Docker image for release
run: |
set -ue
git clean -fd
mkdir -p dist/
docker buildx build --platform linux/amd64,linux/arm64 --push -t ${IMAGE_NAMESPACE}/argocd:${TARGET_VERSION} -t argoproj/argocd:${TARGET_VERSION} .
docker buildx build --platform linux/amd64,linux/arm64 --push -t ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION} -t argoproj/argocd:v${TARGET_VERSION} .
make release-cli
chmod +x ./dist/argocd-linux-amd64
./dist/argocd-linux-amd64 version --client
@@ -287,6 +289,47 @@ jobs:
asset_content_type: application/octet-stream
if: ${{ env.DRY_RUN != 'true' }}
- name: Generate SBOM (spdx)
id: spdx-builder
env:
# defines the spdx/spdx-sbom-generator version to use.
SPDX_GEN_VERSION: v0.0.13
# defines the sigs.k8s.io/bom version to use.
SIGS_BOM_VERSION: v0.2.1
# comma delimited list of project relative folders to inspect for package
# managers (gomod, yarn, npm).
PROJECT_FOLDERS: ".,./ui"
# full qualified name of the docker image to be inspected
DOCKER_IMAGE: ${{env.IMAGE_NAMESPACE}}/argocd:v${{env.TARGET_VERSION}}
run: |
go install github.com/spdx/spdx-sbom-generator/cmd/generator@$SPDX_GEN_VERSION
go install sigs.k8s.io/bom/cmd/bom@$SIGS_BOM_VERSION
# Generate SPDX for project dependencies analyzing package managers
for folder in $(echo $PROJECT_FOLDERS | sed "s/,/ /g")
do
generator -p $folder -o /tmp
done
# Generate SPDX for binaries analyzing the docker image
if [[ ! -z $DOCKER_IMAGE ]]; then
bom generate -o /tmp/bom-docker-image.spdx -i $DOCKER_IMAGE
fi
tar -zcf /tmp/sbom.tar.gz /tmp/*.spdx
if: ${{ env.DRY_RUN != 'true' }}
- name: Upload SBOM to release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: /tmp/sbom.tar.gz
asset_name: sbom.tar.gz
asset_content_type: application/octet-stream
if: ${{ env.DRY_RUN != 'true' }}
- name: Update homebrew formula
env:
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
@@ -301,3 +344,4 @@ jobs:
set -ue
git push --delete origin ${SOURCE_TAG}
if: ${{ always() }}

2
.gitpod.Dockerfile vendored
View File

@@ -9,7 +9,7 @@ RUN curl -L https://go.kubebuilder.io/dl/2.3.1/$(go env GOOS)/$(go env GOARCH) |
tar -xz -C /tmp/ && mv /tmp/kubebuilder_2.3.1_$(go env GOOS)_$(go env GOARCH) /usr/local/kubebuilder
RUN apt-get install redis-server -y
RUN go get github.com/mattn/goreman
RUN go install github.com/mattn/goreman@latest
USER gitpod

View File

@@ -2,5 +2,5 @@ image:
file: .gitpod.Dockerfile
tasks:
- init: make mod-download-local dep-ui-local && GO111MODULE=off go get github.com/mattn/goreman
- init: make mod-download-local dep-ui-local && GO111MODULE=off go install github.com/mattn/goreman@latest
command: make start-test-k8s

View File

@@ -1 +1 @@
2.3.0
2.3.0-rc4

View File

@@ -9,7 +9,6 @@ import (
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/argoproj/pkg/rand"
@@ -68,7 +67,7 @@ func runCommand(ctx context.Context, command Command, path string, env []string)
cmd.Stderr = &stderr
// Make sure the command is killed immediately on timeout. https://stackoverflow.com/a/38133948/684776
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
cmd.SysProcAttr = newSysProcAttr(true)
start := time.Now()
err = cmd.Start()
@@ -80,7 +79,7 @@ func runCommand(ctx context.Context, command Command, path string, env []string)
<-ctx.Done()
// Kill by group ID to make sure child processes are killed. The - tells `kill` that it's a group ID.
// Since we didn't set Pgid in SysProcAttr, the group ID is the same as the process ID. https://pkg.go.dev/syscall#SysProcAttr
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
_ = sysCallKill(-cmd.Process.Pid)
}()
err = cmd.Wait()

View File

@@ -0,0 +1,16 @@
//go:build !windows
// +build !windows
package plugin
import (
"syscall"
)
func newSysProcAttr(setpgid bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{Setpgid: setpgid}
}
func sysCallKill(pid int) error {
return syscall.Kill(pid, syscall.SIGKILL)
}

View File

@@ -0,0 +1,16 @@
//go:build windows
// +build windows
package plugin
import (
"syscall"
)
func newSysProcAttr(setpgid bool) *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}
func sysCallKill(pid int) error {
return nil
}

View File

@@ -36,9 +36,6 @@ import (
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
// make sure to register workqueue prometheus metrics
_ "k8s.io/component-base/metrics/prometheus/workqueue"
statecache "github.com/argoproj/argo-cd/v2/controller/cache"
"github.com/argoproj/argo-cd/v2/controller/metrics"
"github.com/argoproj/argo-cd/v2/pkg/apis/application"

View File

@@ -159,6 +159,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil
mux := http.NewServeMux()
registry := NewAppRegistry(appLister, appFilter, appLabels)
registry.MustRegister(depth, adds, latency, workDuration, unfinished, longestRunningProcessor, retries)
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
// contains app controller specific metrics
registry,

View File

@@ -0,0 +1,101 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"k8s.io/client-go/util/workqueue"
)
const (
WorkQueueSubsystem = "workqueue"
DepthKey = "depth"
AddsKey = "adds_total"
QueueLatencyKey = "queue_duration_seconds"
WorkDurationKey = "work_duration_seconds"
UnfinishedWorkKey = "unfinished_work_seconds"
LongestRunningProcessorKey = "longest_running_processor_seconds"
RetriesKey = "retries_total"
)
var (
depth = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: WorkQueueSubsystem,
Name: DepthKey,
Help: "Current depth of workqueue",
}, []string{"name"})
adds = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: WorkQueueSubsystem,
Name: AddsKey,
Help: "Total number of adds handled by workqueue",
}, []string{"name"})
latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: WorkQueueSubsystem,
Name: QueueLatencyKey,
Help: "How long in seconds an item stays in workqueue before being requested",
Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180},
}, []string{"name"})
workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Subsystem: WorkQueueSubsystem,
Name: WorkDurationKey,
Help: "How long in seconds processing an item from workqueue takes.",
Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180},
}, []string{"name"})
unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: WorkQueueSubsystem,
Name: UnfinishedWorkKey,
Help: "How many seconds of work has been done that " +
"is in progress and hasn't been observed by work_duration. Large " +
"values indicate stuck threads. One can deduce the number of stuck " +
"threads by observing the rate at which this increases.",
}, []string{"name"})
longestRunningProcessor = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Subsystem: WorkQueueSubsystem,
Name: LongestRunningProcessorKey,
Help: "How many seconds has the longest running " +
"processor for workqueue been running.",
}, []string{"name"})
retries = prometheus.NewCounterVec(prometheus.CounterOpts{
Subsystem: WorkQueueSubsystem,
Name: RetriesKey,
Help: "Total number of retries handled by workqueue",
}, []string{"name"})
)
func init() {
workqueue.SetProvider(workqueueMetricsProvider{})
}
type workqueueMetricsProvider struct{}
func (workqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
return depth.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric {
return adds.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric {
return latency.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric {
return workDuration.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric {
return unfinished.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric {
return longestRunningProcessor.WithLabelValues(name)
}
func (workqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric {
return retries.WithLabelValues(name)
}

View File

@@ -1,5 +1,14 @@
# v2.2 to 2.3
## Argo CD Notifications and ApplicationSet Are Bundled into Argo CD
The Argo CD Notifications and ApplicationSet are part of Argo CD now. You no longer need to install them separately.
The Notifications and ApplicationSet components are bundled into default Argo CD installation manifests.
The bundled manifests are drop-in replacements for the previous versions. If you are using Kustomize to bundle the manifests together then just
remove references to https://github.com/argoproj-labs/argocd-notifications and https://github.com/argoproj-labs/applicationset. No action is required
if you are using `kubectl apply`.
## Configure Additional ArgoCD Binaries
We have removed non-Linux ArgoCD binaries (Darwin amd64 and Windows amd64) from the image ([#7668](https://github.com/argoproj/argo-cd/pull/7668)) and the associated download buttons in the help page in the UI.

View File

@@ -235,7 +235,7 @@ p, proj:my-project:admin, repositories, update, my-project/*, allow
This provides extra flexibility so that admins can have stricter rules. e.g.:
```
p, proj:my-project:admin, repositories, update, my-project/"https://github.my-company.com/*", allow
p, proj:my-project:admin, repositories, update, my-project/https://github.my-company.com/*, allow
```
Once the appropriate RBAC rules are in place, developers can create their own Git repositories and (assuming

3
go.mod
View File

@@ -81,7 +81,6 @@ require (
k8s.io/apimachinery v0.23.1
k8s.io/client-go v0.23.1
k8s.io/code-generator v0.23.1
k8s.io/component-base v0.23.1
k8s.io/klog/v2 v2.30.0
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
k8s.io/kubectl v0.23.1
@@ -112,7 +111,6 @@ require (
github.com/antonmedv/expr v1.8.9 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -208,6 +206,7 @@ require (
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/apiserver v0.23.1 // indirect
k8s.io/cli-runtime v0.23.1 // indirect
k8s.io/component-base v0.23.1 // indirect
k8s.io/component-helpers v0.23.1 // indirect
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c // indirect
k8s.io/kube-aggregator v0.23.1 // indirect

1
go.sum
View File

@@ -160,7 +160,6 @@ github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edY
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bombsimon/logrusr/v2 v2.0.1 h1:1VgxVNQMCvjirZIYaT9JYn6sAVGVEcNtRE0y4mvaOAM=

View File

@@ -1,7 +1,7 @@
#!/bin/bash
set -eux -o pipefail
which go-junit-report || go get github.com/jstemmer/go-junit-report
which go-junit-report || go install github.com/jstemmer/go-junit-report@latest
TEST_RESULTS=${TEST_RESULTS:-test-results}
TEST_FLAGS=

View File

@@ -27,23 +27,35 @@ if [ "$IMAGE_TAG" = "" ]; then
IMAGE_TAG=latest
fi
# bundle_with_addons bundles given kustomize base with either stable or latest version of addons
function bundle_with_addons() {
for addon in $(ls $SRCROOT/manifests/addons | grep -v README.md); do
ADDON_BASE="latest"
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ $branch = release-* ]]; then
ADDON_BASE="stable"
fi
rm -rf $SRCROOT/manifests/_tmp-bundle && mkdir -p $SRCROOT/manifests/_tmp-bundle
cat << EOF >> $SRCROOT/manifests/_tmp-bundle/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../$1
- ../addons/$addon/$ADDON_BASE
EOF
echo "${AUTOGENMSG}" > $2
$KUSTOMIZE build $SRCROOT/manifests/_tmp-bundle >> $2
done
}
$KUSTOMIZE version
cd ${SRCROOT}/manifests/base && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
cd ${SRCROOT}/manifests/ha/base && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
cd ${SRCROOT}/manifests/core-install && $KUSTOMIZE edit set image quay.io/argoproj/argocd=${IMAGE_NAMESPACE}/argocd:${IMAGE_TAG}
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/cluster-install" >> "${SRCROOT}/manifests/install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/namespace-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/namespace-install" >> "${SRCROOT}/manifests/namespace-install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/ha/cluster-install" >> "${SRCROOT}/manifests/ha/install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/namespace-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/ha/namespace-install" >> "${SRCROOT}/manifests/ha/namespace-install.yaml"
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/core-install.yaml"
$KUSTOMIZE build "${SRCROOT}/manifests/core-install" >> "${SRCROOT}/manifests/core-install.yaml"
bundle_with_addons "cluster-install" "${SRCROOT}/manifests/install.yaml"
bundle_with_addons "namespace-install" "${SRCROOT}/manifests/namespace-install.yaml"
bundle_with_addons "ha/cluster-install" "${SRCROOT}/manifests/ha/install.yaml"
bundle_with_addons "ha/namespace-install" "${SRCROOT}/manifests/ha/namespace-install.yaml"
bundle_with_addons "core-install" "${SRCROOT}/manifests/core-install.yaml"

1
manifests/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_tmp-bundle

View File

@@ -0,0 +1,5 @@
# Addons
Directory contains Kustomize manifests of bundled Argo CD addons. Each directory must include the latest and stable versions
of the installation manifests in the directories named accordingly. The stable version should point to a particular git
tag and must be updated prior to each release.

View File

@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://raw.githubusercontent.com/argoproj/applicationset/v0.3.0/manifests/install.yaml

View File

@@ -5,13 +5,12 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.3.0-rc4
resources:
- ./application-controller
- ./dex
- ./repo-server
- ./server
- ./applicationset
- ./config
- ./redis
- ./notification

File diff suppressed because it is too large Load Diff

View File

@@ -11,4 +11,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.3.0-rc4

View File

@@ -11,13 +11,12 @@ patchesStrategicMerge:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.3.0-rc4
resources:
- ../../base/application-controller
- ../../base/dex
- ../../base/repo-server
- ../../base/server
- ../../base/applicationset
- ../../base/config
- ../../base/notification
- ./redis-ha

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -52,7 +52,6 @@ import (
"github.com/argoproj/argo-cd/v2/util/io"
"github.com/argoproj/argo-cd/v2/util/ksonnet"
"github.com/argoproj/argo-cd/v2/util/kustomize"
"github.com/argoproj/argo-cd/v2/util/security"
"github.com/argoproj/argo-cd/v2/util/text"
)
@@ -66,6 +65,9 @@ const (
ociPrefix = "oci://"
)
// List of protocol schemes allowed for fetching remote value files
var allowedHelmRemoteProtocols = []string{"http", "https"}
// Service implements ManifestService interface
type Service struct {
repoLock *repositoryLock
@@ -554,6 +556,146 @@ func runHelmBuild(appPath string, h helm.Helm) error {
return ioutil.WriteFile(markerFile, []byte("marker"), 0644)
}
// resolveSymbolicLinkRecursive resolves the symlink path recursively to its
// canonical path on the file system, with a maximum nesting level of maxDepth.
// If path is not a symlink, returns the verbatim copy of path and err of nil.
func resolveSymbolicLinkRecursive(path string, maxDepth int) (string, error) {
resolved, err := os.Readlink(path)
if err != nil {
// path is not a symbolic link
_, ok := err.(*os.PathError)
if ok {
return path, nil
}
// Other error has occured
return "", err
}
if maxDepth == 0 {
return "", fmt.Errorf("maximum nesting level reached")
}
return resolveSymbolicLinkRecursive(resolved, maxDepth-1)
}
// isURLSchemeAllowed returns true if the protocol scheme is in the list of
// allowed URL schemes.
func isURLSchemeAllowed(scheme string, allowed []string) bool {
isAllowed := false
if len(allowed) > 0 {
for _, s := range allowed {
if strings.EqualFold(scheme, s) {
isAllowed = true
break
}
}
}
// Empty scheme means local file
return isAllowed && scheme != ""
}
// resolveHelmValueFilePath will inspect and resolve a path to a Helm value
// file, and make sure that its final path is within the boundaries of the
// path specified in repoRoot.
//
// appPath is the path we're operating in, e.g. where a Helm chart was unpacked
// to. repoRoot is the path to the root of the repository.
//
// If either appPath or repoRoot is relative, it will be treated as relative
// to the current working directory.
//
// valueFile is the path to a value file, relative to appPath. If valueFile is
// specified as an absolute path (i.e. leading slash), it will be treated as
// relative to the repoRoot. In case valueFile is a symlink in the extracted
// chart, it will be resolved recursively and the decision of whether it is in
// the boundary of repoRoot will be made using the final resolved path.
// valueFile can also be a remote URL with a protocol scheme as prefix,
// in which case the scheme must be included in the list of allowed schemes
// specified by allowedURLSchemes.
//
// Will return an error if either valueFile is outside the boundaries of the
// repoRoot, valueFile is an URL with a forbidden protocol scheme or if
// valueFile is a recursive symlink nested too deep. May return errors for
// other reasons as well.
//
// resolvedPath will hold the absolute, resolved path for valueFile on success
// or set to the empty string on failure.
//
// isRemote will be set to true if valueFile is an URL using an allowed
// protocol scheme, or to false if it resolved to a local file.
func resolveHelmValueFilePath(appPath, repoRoot, valueFile string, allowedURLSchemes []string) (resolvedPath string, isRemote bool, err error) {
// We do not provide the path in the error message, because it will be
// returned to the user and could be used for information gathering.
// Instead, we log the concrete error details.
resolveFailure := func(path string, err error) error {
log.Errorf("failed to resolve path '%s': %v", path, err)
return fmt.Errorf("internal error: failed to resolve path. Check logs for more details")
}
// A value file can be specified as an URL to a remote resource.
// We only allow certain URL schemes for remote value files.
url, err := url.Parse(valueFile)
if err == nil {
// If scheme is empty, it means we parsed a path only
if url.Scheme != "" {
if isURLSchemeAllowed(url.Scheme, allowedURLSchemes) {
return valueFile, true, nil
} else {
return "", false, fmt.Errorf("the URL scheme '%s' is not allowed", url.Scheme)
}
}
}
// Ensure that our repository root is absolute
absRepoPath, err := filepath.Abs(repoRoot)
if err != nil {
return "", false, resolveFailure(repoRoot, err)
}
// If the path to the file is relative, join it with the current working directory (appPath)
// Otherwise, join it with the repository's root
path := valueFile
if !filepath.IsAbs(path) {
absWorkDir, err := filepath.Abs(appPath)
if err != nil {
return "", false, resolveFailure(repoRoot, err)
}
path = filepath.Join(absWorkDir, path)
} else {
path = filepath.Join(absRepoPath, path)
}
// Ensure any symbolic link is resolved before we
delinkedPath, err := resolveSymbolicLinkRecursive(path, 10)
if err != nil {
return "", false, resolveFailure(path, err)
}
path = delinkedPath
// Resolve the joined path to an absolute path
path, err = filepath.Abs(path)
if err != nil {
return "", false, resolveFailure(path, err)
}
// Ensure our root path has a trailing slash, otherwise the following check
// would return true if root is /foo and path would be /foo2
requiredRootPath := absRepoPath
if !strings.HasSuffix(requiredRootPath, "/") {
requiredRootPath += "/"
}
// Make sure that the resolved path to values file is within the repository's root path
if !strings.HasPrefix(path, requiredRootPath) {
return "", false, fmt.Errorf("value file '%s' resolved to outside repository root", valueFile)
}
return path, false, nil
}
func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclient.ManifestRequest, isLocal bool) ([]*unstructured.Unstructured, error) {
concurrencyAllowed := isConcurrencyAllowed(appPath)
if !concurrencyAllowed {
@@ -583,31 +725,14 @@ func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclie
}
for _, val := range appHelm.ValueFiles {
// If val is not a URL, run it against the directory enforcer. If it is a URL, use it without checking
// If val does not exist, warn. If IgnoreMissingValueFiles, do not append, else let Helm handle it.
if _, err := url.ParseRequestURI(val); err != nil {
// Ensure that the repo root provided is absolute
absRepoPath, err := filepath.Abs(repoRoot)
if err != nil {
return nil, err
}
// If the path to the file is relative, join it with the current working directory (appPath)
path := val
if !filepath.IsAbs(path) {
absWorkDir, err := filepath.Abs(appPath)
if err != nil {
return nil, err
}
path = filepath.Join(absWorkDir, path)
}
_, err = security.EnforceToCurrentRoot(absRepoPath, path)
if err != nil {
return nil, err
}
// This will resolve val to an absolute path (or an URL)
path, isRemote, err := resolveHelmValueFilePath(appPath, repoRoot, val, allowedHelmRemoteProtocols)
if err != nil {
return nil, err
}
if !isRemote {
_, err = os.Stat(path)
if os.IsNotExist(err) {
if appHelm.IgnoreMissingValueFiles {
@@ -616,7 +741,8 @@ func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclie
}
}
}
templateOpts.Values = append(templateOpts.Values, val)
templateOpts.Values = append(templateOpts.Values, path)
}
if appHelm.Values != "" {

View File

@@ -132,7 +132,7 @@ func TestGenerateYamlManifestInDir(t *testing.T) {
q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src}
// update this value if we add/remove manifests
const countOfManifests = 47
const countOfManifests = 41
res1, err := service.GenerateManifest(context.Background(), &q)
@@ -754,7 +754,7 @@ func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) {
}
request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true}
_, err := service.GenerateManifest(context.Background(), request)
assert.Error(t, err, "should be on or under current directory")
assert.Error(t, err)
}
func TestGenerateHelmWithURL(t *testing.T) {
@@ -777,33 +777,88 @@ func TestGenerateHelmWithURL(t *testing.T) {
// The requested value file (`../../../../../minio/values.yaml`) is outside the repo directory
// (`~/go/src/github.com/argoproj/argo-cd`), so it is blocked
func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) {
service := newService("../..")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: "./util/helm/testdata/redis",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"../../../../../minio/values.yaml"},
Values: `cluster: {slaveCount: 2}`,
t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) {
service := newService("../..")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: "./util/helm/testdata/redis",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"../../../../../minio/values.yaml"},
Values: `cluster: {slaveCount: 2}`,
},
},
},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
})
assert.Error(t, err, "should be on or under current directory")
service = newService("./testdata/my-chart")
_, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: ".",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"../my-chart-2/values.yaml"},
Values: `cluster: {slaveCount: 2}`,
t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) {
service := newService("./testdata/my-chart")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: ".",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"../my-chart/my-chart-values.yaml"},
Values: `cluster: {slaveCount: 2}`,
},
},
},
})
assert.NoError(t, err)
})
t.Run("Values file with absolute path stays within repo root", func(t *testing.T) {
service := newService("./testdata/my-chart")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: ".",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"/my-chart-values.yaml"},
Values: `cluster: {slaveCount: 2}`,
},
},
})
assert.NoError(t, err)
})
t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) {
service := newService("./testdata/my-chart")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: ".",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"/../../../my-chart-values.yaml"},
Values: `cluster: {slaveCount: 2}`,
},
},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
})
t.Run("Remote values file from forbidden protocol", func(t *testing.T) {
service := newService("./testdata/my-chart")
_, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{},
AppName: "test",
ApplicationSource: &argoappv1.ApplicationSource{
Path: ".",
Helm: &argoappv1.ApplicationSourceHelm{
ValueFiles: []string{"file://../../../../my-chart-values.yaml"},
Values: `cluster: {slaveCount: 2}`,
},
},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "is not allowed")
})
assert.Error(t, err, "should be on or under current directory")
}
// The requested file parameter (`/tmp/external-secret.txt`) is outside the app path
@@ -1631,3 +1686,178 @@ func TestResolveRevisionNegativeScenarios(t *testing.T) {
assert.Equal(t, expectedResolveRevisionResponse, resolveRevisionResponse)
}
func Test_resolveSymlinkRecursive(t *testing.T) {
cwd, err := os.Getwd()
require.NoError(t, err)
err = os.Chdir("testdata/symlinks")
require.NoError(t, err)
defer func() {
err := os.Chdir(cwd)
if err != nil {
panic(err)
}
}()
t.Run("Resolve non-symlink", func(t *testing.T) {
r, err := resolveSymbolicLinkRecursive("foo", 2)
assert.NoError(t, err)
assert.Equal(t, "foo", r)
})
t.Run("Successfully resolve symlink", func(t *testing.T) {
r, err := resolveSymbolicLinkRecursive("bar", 2)
assert.NoError(t, err)
assert.Equal(t, "foo", r)
})
t.Run("Do not allow symlink at all", func(t *testing.T) {
r, err := resolveSymbolicLinkRecursive("bar", 0)
assert.Error(t, err)
assert.Equal(t, "", r)
})
t.Run("Error because too nested symlink", func(t *testing.T) {
r, err := resolveSymbolicLinkRecursive("bam", 2)
assert.Error(t, err)
assert.Equal(t, "", r)
})
t.Run("No such file or directory", func(t *testing.T) {
r, err := resolveSymbolicLinkRecursive("foobar", 2)
assert.NoError(t, err)
assert.Equal(t, "foobar", r)
})
}
func Test_isURLSchemeAllowed(t *testing.T) {
type testdata struct {
name string
scheme string
allowed []string
expected bool
}
var tts []testdata = []testdata{
{
name: "Allowed scheme matches",
scheme: "http",
allowed: []string{"http", "https"},
expected: true,
},
{
name: "Allowed scheme matches only partially",
scheme: "http",
allowed: []string{"https"},
expected: false,
},
{
name: "Scheme is not allowed",
scheme: "file",
allowed: []string{"http", "https"},
expected: false,
},
{
name: "Empty scheme with valid allowances is forbidden",
scheme: "",
allowed: []string{"http", "https"},
expected: false,
},
{
name: "Empty scheme with empty allowances is forbidden",
scheme: "",
allowed: []string{},
expected: false,
},
{
name: "Some scheme with empty allowances is forbidden",
scheme: "file",
allowed: []string{},
expected: false,
},
}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
r := isURLSchemeAllowed(tt.scheme, tt.allowed)
assert.Equal(t, tt.expected, r)
})
}
}
func Test_resolveHelmValueFilePath(t *testing.T) {
t.Run("Resolve normal relative path into absolute path", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", "baz/bim.yaml", allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.False(t, remote)
assert.Equal(t, "/foo/bar/baz/bim.yaml", p)
})
t.Run("Resolve normal relative path into absolute path", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", "baz/../../bim.yaml", allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.False(t, remote)
assert.Equal(t, "/foo/bim.yaml", p)
})
t.Run("Error on path resolving outside repository root", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", "baz/../../../bim.yaml", allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
assert.False(t, remote)
assert.Equal(t, "", p)
})
t.Run("Return verbatim URL", func(t *testing.T) {
url := "https://some.where/foo,yaml"
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", url, allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.True(t, remote)
assert.Equal(t, url, p)
})
t.Run("URL scheme not allowed", func(t *testing.T) {
url := "file:///some.where/foo,yaml"
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", url, allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.False(t, remote)
assert.Equal(t, "", p)
})
t.Run("Implicit URL by absolute path", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath("/foo/bar", "/foo", "/baz.yaml", allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.False(t, remote)
assert.Equal(t, "/foo/baz.yaml", p)
})
t.Run("Relative app path", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath(".", "/foo", "/baz.yaml", allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.False(t, remote)
assert.Equal(t, "/foo/baz.yaml", p)
})
t.Run("Relative repo path", func(t *testing.T) {
c, err := os.Getwd()
require.NoError(t, err)
p, remote, err := resolveHelmValueFilePath(".", ".", "baz.yaml", allowedHelmRemoteProtocols)
assert.NoError(t, err)
assert.False(t, remote)
assert.Equal(t, c+"/baz.yaml", p)
})
t.Run("Overlapping root prefix without trailing slash", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath(".", "/foo", "../foo2/baz.yaml", allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
assert.False(t, remote)
assert.Equal(t, "", p)
})
t.Run("Overlapping root prefix with trailing slash", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath(".", "/foo/", "../foo2/baz.yaml", allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
assert.False(t, remote)
assert.Equal(t, "", p)
})
t.Run("Garbage input as values file", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath(".", "/foo/", "kfdj\\ks&&&321209.,---e32908923%$§!\"", allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
assert.False(t, remote)
assert.Equal(t, "", p)
})
t.Run("NUL-byte path input as values file", func(t *testing.T) {
p, remote, err := resolveHelmValueFilePath(".", "/foo/", "\000", allowedHelmRemoteProtocols)
assert.Error(t, err)
assert.Contains(t, err.Error(), "outside repository root")
assert.False(t, remote)
assert.Equal(t, "", p)
})
}

View File

@@ -0,0 +1 @@
baz

View File

@@ -0,0 +1 @@
foo

View File

@@ -0,0 +1 @@
bar

View File

@@ -0,0 +1 @@
hello

View File

@@ -49,9 +49,9 @@ RUN ./install.sh ksonnet-linux && \
./install.sh codegen-tools && \
./install.sh codegen-go-tools && \
./install.sh lint-tools && \
go get github.com/mattn/goreman && \
go get github.com/kisielk/godepgraph && \
go get github.com/jstemmer/go-junit-report && \
go install github.com/mattn/goreman@latest && \
go install github.com/kisielk/godepgraph@latest && \
go install github.com/jstemmer/go-junit-report@latest && \
rm -rf /tmp/dl && \
rm -rf /tmp/helm && \
rm -rf /tmp/helm2 && \

View File

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

View File

@@ -54,7 +54,7 @@ To build it, run the following. Note that kustomize is required:
```shell
cd test/remote
export IMAGE_NAMESPACE=quay.io/youruser
export IMAGE_NAMESPACE=quay.io/{YOUR USERNAME HERE}
# builds & tags the image
make image
# pushes the image to your repository
@@ -66,6 +66,8 @@ make manifests > /tmp/e2e-repositories.yaml
If you do not have kustomize installed, you need to manually edit the manifests
at `test/remote/manifests/e2e_repositories.yaml` to point to the correct image.
If you get `make: realpath: Command not found`, install coreutils.
### Deploy the test container and additional permissions
**Note:** The test container requires to be run in privileged mode for now, due
@@ -83,7 +85,7 @@ Then, apply the manifests for the E2E repositories workload:
kubectl -n argocd-e2e apply -f /tmp/e2e-repositories.yaml
```
Verify that the deployment was succesful:
Verify that the deployment was successful:
```shell
kubectl -n argocd-e2e rollout status deployment argocd-e2e-cluster
@@ -106,13 +108,13 @@ as the cluster, or the cluster IPs are routed to your host, you can use the
following:
```shell
export ARGOCD_SERVER=$(kubectl get svc argocd-server -o jsonpath='{.spec.clusterIP}')
export ARGOCD_SERVER=$(kubectl -n argocd-e2e get svc argocd-server -o jsonpath='{.spec.clusterIP}')
```
Set the admin password to use:
```shell
export ARGOCD_E2E_ADMIN_PASSWORD=$(kubectl get secrets argocd-initial-admin-secret -o jsonpath='{.data.password}'|base64 -d)
export ARGOCD_E2E_ADMIN_PASSWORD=$(kubectl -n argocd-e2e get secrets argocd-initial-admin-secret -o jsonpath='{.data.password}'|base64 -d)
```
Run the tests
@@ -204,7 +206,7 @@ Some environment variables can control the behavior of the tests:
Furthermore, you can skip various classes of tests by setting the following to true:
```
```shell
# If you disabled GPG feature, set to true to skip related tests
export ARGOCD_E2E_SKIP_GPG=${ARGOCD_E2E_SKIP_GPG:-false}
# Some tests do not work OOTB with OpenShift
@@ -215,7 +217,6 @@ export ARGOCD_E2E_SKIP_HELM=${ARGOCD_E2E_SKIP_HELM:-false}
export ARGOCD_E2E_SKIP_HELM2=${ARGOCD_E2E_SKIP_HELM2:-false}
# Skip Ksonnet tests
export ARGOCD_E2E_SKIP_KSONNET=${ARGOCD_E2E_SKIP_KSONNET:-false}
```
## Recording tests that ran successfully and restart at point of fail

View File

@@ -1,6 +1,8 @@
import {Checkbox} from 'argo-ui';
import {useData} from 'argo-ui/v2';
import * as minimatch from 'minimatch';
import * as React from 'react';
import {Context} from '../../../shared/context';
import {Application, ApplicationDestination, Cluster, HealthStatusCode, HealthStatuses, SyncStatusCode, SyncStatuses} from '../../../shared/models';
import {AppsListPreferences, services} from '../../../shared/services';
import {Filter, FiltersGroup} from '../filter/filter';
@@ -13,6 +15,7 @@ export interface FilterResult {
health: boolean;
namespaces: boolean;
clusters: boolean;
favourite: boolean;
labels: boolean;
}
@@ -28,6 +31,7 @@ export function getFilterResults(applications: Application[], pref: AppsListPref
sync: pref.syncFilter.length === 0 || pref.syncFilter.includes(app.status.sync.status),
health: pref.healthFilter.length === 0 || pref.healthFilter.includes(app.status.health.status),
namespaces: pref.namespacesFilter.length === 0 || pref.namespacesFilter.some(ns => app.spec.destination.namespace && minimatch(app.spec.destination.namespace, ns)),
favourite: !pref.showFavorites || (pref.favoritesAppList && pref.favoritesAppList.includes(app.metadata.name)),
clusters:
pref.clustersFilter.length === 0 ||
pref.clustersFilter.some(filterString => {
@@ -211,6 +215,23 @@ const NamespaceFilter = (props: AppFilterProps) => {
);
};
const FavoriteFilter = (props: AppFilterProps) => {
const ctx = React.useContext(Context);
return (
<div className='filter'>
<Checkbox
checked={!!props.pref.showFavorites}
id='favouriteFilter'
onChange={val => {
ctx.navigation.goto('.', {showFavorites: val}, {replace: true});
services.viewPreferences.updatePreferences({appList: {...props.pref, showFavorites: val}});
}}
/>{' '}
<label htmlFor='favouriteFilter'>FAVORITES ONLY</label>
</div>
);
};
export const ApplicationsFilter = (props: AppFilterProps) => {
const setShown = (val: boolean) => {
services.viewPreferences.updatePreferences({appList: {...props.pref, hideFilters: !val}});
@@ -218,6 +239,7 @@ export const ApplicationsFilter = (props: AppFilterProps) => {
return (
<FiltersGroup setShown={setShown} expanded={!props.pref.hideFilters} content={props.children}>
<FavoriteFilter {...props} />
<SyncFilter {...props} />
<HealthFilter {...props} />
<LabelsFilter {...props} />

View File

@@ -172,7 +172,6 @@
&__external-links-icon-container {
position: relative;
display: inline-block;
width: 28px;
}
.filters-group__panel {

View File

@@ -122,6 +122,9 @@ const ViewPref = ({children}: {children: (pref: AppsListPreferences & {page: num
.split(',')
.filter(item => !!item);
}
if (params.get('showFavorites') != null) {
viewPref.showFavorites = params.get('showFavorites') === 'true';
}
if (params.get('view') != null) {
viewPref.view = params.get('view') as AppsListViewType;
} else {

View File

@@ -1,4 +1,4 @@
import {DropDownMenu} from 'argo-ui';
import {DataLoader, DropDownMenu, Tooltip} from 'argo-ui';
import * as React from 'react';
import {Key, KeybindingContext, useNav} from 'argo-ui/v2';
import {Cluster} from '../../../shared/components';
@@ -9,6 +9,7 @@ import * as AppUtils from '../utils';
import {OperationState} from '../utils';
import {ApplicationsLabels} from './applications-labels';
import {ApplicationsSource} from './applications-source';
import {services} from '../../../shared/services';
require('./applications-table.scss');
export const ApplicationsTable = (props: {
@@ -34,66 +35,98 @@ export const ApplicationsTable = (props: {
return (
<Consumer>
{ctx => (
<div className='applications-table argo-table-list argo-table-list--clickable'>
{props.applications.map((app, i) => (
<div
key={app.metadata.name}
className={`argo-table-list__row
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{pref => {
const favList = pref.appList.favoritesAppList || [];
return (
<div className='applications-table argo-table-list argo-table-list--clickable'>
{props.applications.map((app, i) => (
<div
key={app.metadata.name}
className={`argo-table-list__row
applications-list__entry applications-list__entry--health-${app.status.health.status} ${selectedApp === i ? 'applications-tiles__selected' : ''}`}>
<div className={`row applications-list__table-row`} onClick={e => ctx.navigation.goto(`/applications/${app.metadata.name}`, {}, {event: e})}>
<div className='columns small-4'>
<div className='row'>
<div className='show-for-xxlarge columns small-3'>Project:</div>
<div className='columns small-12 xxlarge-9'>{app.spec.project}</div>
</div>
<div className='row'>
<div className='show-for-xxlarge columns small-3'>Name:</div>
<div className='columns small-12 xxlarge-9'>
{app.metadata.name} <ApplicationURLs urls={AppUtils.getExternalUrls(app.metadata.annotations, app.status.summary.externalURLs)} />
</div>
</div>
</div>
<div className='columns small-6'>
<div className='row'>
<div className='show-for-xxlarge columns small-2'>Source:</div>
<div className='columns small-12 xxlarge-10 applications-table-source' style={{position: 'relative'}}>
<div className='applications-table-source__link'>
<ApplicationsSource source={app.spec.source} />
<div
className={`row applications-list__table-row`}
onClick={e => ctx.navigation.goto(`/applications/${app.metadata.name}`, {}, {event: e})}>
<div className='columns small-4'>
<div className='row'>
<div className=' columns small-2'>
<div>
<Tooltip content={favList?.includes(app.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}>
<button
onClick={e => {
e.stopPropagation();
favList?.includes(app.metadata.name)
? favList.splice(favList.indexOf(app.metadata.name), 1)
: favList.push(app.metadata.name);
services.viewPreferences.updatePreferences({appList: {...pref.appList, favoritesAppList: favList}});
}}>
<i
className={'fas fa-star'}
style={{
cursor: 'pointer',
marginRight: '7px',
color: favList?.includes(app.metadata.name) ? '#1FBDD0' : 'grey'
}}
/>
</button>
</Tooltip>
<ApplicationURLs urls={AppUtils.getExternalUrls(app.metadata.annotations, app.status.summary.externalURLs)} />
</div>
</div>
<div className='show-for-xxlarge columns small-4'>Project:</div>
<div className='columns small-12 xxlarge-6'>{app.spec.project}</div>
</div>
<div className='row'>
<div className=' columns small-2' />
<div className='show-for-xxlarge columns small-4'>Name:</div>
<div className='columns small-12 xxlarge-6'>{app.metadata.name}</div>
</div>
</div>
<div className='applications-table-source__labels'>
<ApplicationsLabels app={app} />
<div className='columns small-6'>
<div className='row'>
<div className='show-for-xxlarge columns small-2'>Source:</div>
<div className='columns small-12 xxlarge-10 applications-table-source' style={{position: 'relative'}}>
<div className='applications-table-source__link'>
<ApplicationsSource source={app.spec.source} />
</div>
<div className='applications-table-source__labels'>
<ApplicationsLabels app={app} />
</div>
</div>
</div>
<div className='row'>
<div className='show-for-xxlarge columns small-2'>Destination:</div>
<div className='columns small-12 xxlarge-10'>
<Cluster server={app.spec.destination.server} name={app.spec.destination.name} />/{app.spec.destination.namespace}
</div>
</div>
</div>
<div className='columns small-2'>
<AppUtils.HealthStatusIcon state={app.status.health} /> <span>{app.status.health.status}</span> <br />
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} />
<span>{app.status.sync.status}</span> <OperationState app={app} quiet={true} />
<DropDownMenu
anchor={() => (
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
<i className='fa fa-ellipsis-v' />
</button>
)}
items={[
{title: 'Sync', action: () => props.syncApplication(app.metadata.name)},
{title: 'Refresh', action: () => props.refreshApplication(app.metadata.name)},
{title: 'Delete', action: () => props.deleteApplication(app.metadata.name)}
]}
/>
</div>
</div>
</div>
<div className='row'>
<div className='show-for-xxlarge columns small-2'>Destination:</div>
<div className='columns small-12 xxlarge-10'>
<Cluster server={app.spec.destination.server} name={app.spec.destination.name} />/{app.spec.destination.namespace}
</div>
</div>
</div>
<div className='columns small-2'>
<AppUtils.HealthStatusIcon state={app.status.health} /> <span>{app.status.health.status}</span>
<br />
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} />
<span>{app.status.sync.status}</span> <OperationState app={app} quiet={true} />
<DropDownMenu
anchor={() => (
<button className='argo-button argo-button--light argo-button--lg argo-button--short'>
<i className='fa fa-ellipsis-v' />
</button>
)}
items={[
{title: 'Sync', action: () => props.syncApplication(app.metadata.name)},
{title: 'Refresh', action: () => props.refreshApplication(app.metadata.name)},
{title: 'Delete', action: () => props.deleteApplication(app.metadata.name)}
]}
/>
</div>
))}
</div>
</div>
))}
</div>
);
}}
</DataLoader>
)}
</Consumer>
);

View File

@@ -102,158 +102,178 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat
<Consumer>
{ctx => (
<DataLoader load={() => services.viewPreferences.getPreferences()}>
{pref => (
<div className='applications-tiles argo-table-list argo-table-list--clickable row small-up-1 medium-up-2 large-up-3 xxxlarge-up-4' ref={appContainerRef}>
{applications.map((app, i) => (
<div key={app.metadata.name} className='column column-block'>
<div
ref={appRef.set ? null : appRef.ref}
className={`argo-table-list__row applications-list__entry applications-list__entry--health-${app.status.health.status} ${
selectedApp === i ? 'applications-tiles__selected' : ''
}`}>
<div className='row' onClick={e => ctx.navigation.goto(`/applications/${app.metadata.name}`, {view: pref.appDetails.view}, {event: e})}>
<div className={`columns small-12 applications-list__info qe-applications-list-${app.metadata.name}`}>
<div className='applications-list__external-link'>
<ApplicationURLs urls={AppUtils.getExternalUrls(app.metadata.annotations, app.status.summary.externalURLs)} />
</div>
<div className='row'>
<div className='columns small-12'>
<i className={'icon argo-icon-' + (app.spec.source.chart != null ? 'helm' : 'git')} />
<span className='applications-list__title'>{app.metadata.name}</span>
{pref => {
const favList = pref.appList.favoritesAppList || [];
return (
<div
className='applications-tiles argo-table-list argo-table-list--clickable row small-up-1 medium-up-2 large-up-3 xxxlarge-up-4'
ref={appContainerRef}>
{applications.map((app, i) => (
<div key={app.metadata.name} className='column column-block'>
<div
ref={appRef.set ? null : appRef.ref}
className={`argo-table-list__row applications-list__entry applications-list__entry--health-${app.status.health.status} ${
selectedApp === i ? 'applications-tiles__selected' : ''
}`}>
<div className='row' onClick={e => ctx.navigation.goto(`/applications/${app.metadata.name}`, {view: pref.appDetails.view}, {event: e})}>
<div className={`columns small-12 applications-list__info qe-applications-list-${app.metadata.name}`}>
<div className='applications-list__external-link'>
<ApplicationURLs urls={AppUtils.getExternalUrls(app.metadata.annotations, app.status.summary.externalURLs)} />
<Tooltip content={favList?.includes(app.metadata.name) ? 'Remove Favorite' : 'Add Favorite'}>
<button
onClick={e => {
e.stopPropagation();
favList?.includes(app.metadata.name)
? favList.splice(favList.indexOf(app.metadata.name), 1)
: favList.push(app.metadata.name);
services.viewPreferences.updatePreferences({appList: {...pref.appList, favoritesAppList: favList}});
}}>
<i
className={'fas fa-star fa-lg'}
style={{cursor: 'pointer', marginLeft: '7px', color: favList?.includes(app.metadata.name) ? '#1FBDD0' : 'grey'}}
/>
</button>
</Tooltip>
</div>
</div>
<div className='row'>
<div className='columns small-3' title='Project:'>
Project:
<div className='row'>
<div className='columns small-12'>
<i className={'icon argo-icon-' + (app.spec.source.chart != null ? 'helm' : 'git')} />
<span className='applications-list__title'>{app.metadata.name}</span>
</div>
</div>
<div className='columns small-9'>{app.spec.project}</div>
</div>
<div className='row'>
<div className='columns small-3' title='Labels:'>
Labels:
<div className='row'>
<div className='columns small-3' title='Project:'>
Project:
</div>
<div className='columns small-9'>{app.spec.project}</div>
</div>
<div className='columns small-9'>
<Tooltip
zIndex={4}
content={
<div>
<div className='row'>
<div className='columns small-3' title='Labels:'>
Labels:
</div>
<div className='columns small-9'>
<Tooltip
zIndex={4}
content={
<div>
{Object.keys(app.metadata.labels || {})
.map(label => ({label, value: app.metadata.labels[label]}))
.map(item => (
<div key={item.label}>
{item.label}={item.value}
</div>
))}
</div>
}>
<span>
{Object.keys(app.metadata.labels || {})
.map(label => ({label, value: app.metadata.labels[label]}))
.map(item => (
<div key={item.label}>
{item.label}={item.value}
</div>
))}
</div>
}>
<span>
{Object.keys(app.metadata.labels || {})
.map(label => `${label}=${app.metadata.labels[label]}`)
.join(', ')}
</span>
</Tooltip>
</div>
</div>
<div className='row'>
<div className='columns small-3' title='Status:'>
Status:
</div>
<div className='columns small-9' qe-id='applications-tiles-health-status'>
<AppUtils.HealthStatusIcon state={app.status.health} /> {app.status.health.status}
&nbsp;
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status}
&nbsp;
<OperationState app={app} quiet={true} />
</div>
</div>
<div className='row'>
<div className='columns small-3' title='Repository:'>
Repository:
</div>
<div className='columns small-9'>
<Tooltip content={app.spec.source.repoURL} zIndex={4}>
<span>{app.spec.source.repoURL}</span>
</Tooltip>
</div>
</div>
<div className='row'>
<div className='columns small-3' title='Target Revision:'>
Target Revision:
</div>
<div className='columns small-9'>{app.spec.source.targetRevision}</div>
</div>
{app.spec.source.path && (
<div className='row'>
<div className='columns small-3' title='Path:'>
Path:
.map(label => `${label}=${app.metadata.labels[label]}`)
.join(', ')}
</span>
</Tooltip>
</div>
<div className='columns small-9'>{app.spec.source.path}</div>
</div>
)}
{app.spec.source.chart && (
<div className='row'>
<div className='columns small-3' title='Chart:'>
Chart:
<div className='columns small-3' title='Status:'>
Status:
</div>
<div className='columns small-9' qe-id='applications-tiles-health-status'>
<AppUtils.HealthStatusIcon state={app.status.health} /> {app.status.health.status}
&nbsp;
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status}
&nbsp;
<OperationState app={app} quiet={true} />
</div>
<div className='columns small-9'>{app.spec.source.chart}</div>
</div>
)}
<div className='row'>
<div className='columns small-3' title='Destination:'>
Destination:
<div className='row'>
<div className='columns small-3' title='Repository:'>
Repository:
</div>
<div className='columns small-9'>
<Tooltip content={app.spec.source.repoURL} zIndex={4}>
<span>{app.spec.source.repoURL}</span>
</Tooltip>
</div>
</div>
<div className='columns small-9'>
<Cluster server={app.spec.destination.server} name={app.spec.destination.name} />
<div className='row'>
<div className='columns small-3' title='Target Revision:'>
Target Revision:
</div>
<div className='columns small-9'>{app.spec.source.targetRevision}</div>
</div>
</div>
<div className='row'>
<div className='columns small-3' title='Namespace:'>
Namespace:
{app.spec.source.path && (
<div className='row'>
<div className='columns small-3' title='Path:'>
Path:
</div>
<div className='columns small-9'>{app.spec.source.path}</div>
</div>
)}
{app.spec.source.chart && (
<div className='row'>
<div className='columns small-3' title='Chart:'>
Chart:
</div>
<div className='columns small-9'>{app.spec.source.chart}</div>
</div>
)}
<div className='row'>
<div className='columns small-3' title='Destination:'>
Destination:
</div>
<div className='columns small-9'>
<Cluster server={app.spec.destination.server} name={app.spec.destination.name} />
</div>
</div>
<div className='columns small-9'>{app.spec.destination.namespace}</div>
</div>
<div className='row'>
<div className='columns applications-list__entry--actions'>
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-sync'
onClick={e => {
e.stopPropagation();
syncApplication(app.metadata.name);
}}>
<i className='fa fa-sync' /> Sync
</a>
&nbsp;
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-refresh'
{...AppUtils.refreshLinkAttrs(app)}
onClick={e => {
e.stopPropagation();
refreshApplication(app.metadata.name);
}}>
<i className={classNames('fa fa-redo', {'status-icon--spin': AppUtils.isAppRefreshing(app)})} />{' '}
<span className='show-for-xxlarge'>Refresh</span>
</a>
&nbsp;
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-delete'
onClick={e => {
e.stopPropagation();
deleteApplication(app.metadata.name);
}}>
<i className='fa fa-times-circle' /> <span className='show-for-xxlarge'>Delete</span>
</a>
<div className='row'>
<div className='columns small-3' title='Namespace:'>
Namespace:
</div>
<div className='columns small-9'>{app.spec.destination.namespace}</div>
</div>
<div className='row'>
<div className='columns applications-list__entry--actions'>
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-sync'
onClick={e => {
e.stopPropagation();
syncApplication(app.metadata.name);
}}>
<i className='fa fa-sync' /> Sync
</a>
&nbsp;
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-refresh'
{...AppUtils.refreshLinkAttrs(app)}
onClick={e => {
e.stopPropagation();
refreshApplication(app.metadata.name);
}}>
<i className={classNames('fa fa-redo', {'status-icon--spin': AppUtils.isAppRefreshing(app)})} />{' '}
<span className='show-for-xxlarge'>Refresh</span>
</a>
&nbsp;
<a
className='argo-button argo-button--base'
qe-id='applications-tiles-button-delete'
onClick={e => {
e.stopPropagation();
deleteApplication(app.metadata.name);
}}>
<i className='fa fa-times-circle' /> <span className='show-for-xxlarge'>Delete</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
)}
))}
</div>
);
}}
</DataLoader>
)}
</Consumer>

View File

@@ -63,6 +63,7 @@ export class AppsListPreferences {
pref.projectsFilter = [];
pref.reposFilter = [];
pref.syncFilter = [];
pref.showFavorites = false;
}
public labelsFilter: string[];
@@ -75,6 +76,8 @@ export class AppsListPreferences {
public view: AppsListViewType;
public hideFilters: boolean;
public statusBarView: HealthStatusBarPreferences;
public showFavorites: boolean;
public favoritesAppList: string[];
}
export interface ViewPreferences {
@@ -117,6 +120,8 @@ const DEFAULT_PREFERENCES: ViewPreferences = {
syncFilter: new Array<string>(),
healthFilter: new Array<string>(),
hideFilters: false,
showFavorites: false,
favoritesAppList: new Array<string>(),
statusBarView: {
showHealthStatusBar: true
}