mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 09:38:49 +01:00
Compare commits
21 Commits
stable
...
v2.3.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed734fedbb | ||
|
|
7414f2d42c | ||
|
|
8139df8983 | ||
|
|
f364330de2 | ||
|
|
4ec67d8b08 | ||
|
|
3d3f81df4a | ||
|
|
37f01f6f32 | ||
|
|
36eab6b82c | ||
|
|
793acc147f | ||
|
|
b50609c0e6 | ||
|
|
5f48ce96c6 | ||
|
|
c32460a2bc | ||
|
|
7eb1aba99b | ||
|
|
1a8139f4d6 | ||
|
|
02c03c3b26 | ||
|
|
cdb20d5060 | ||
|
|
7d7eed4932 | ||
|
|
af8c5eb07a | ||
|
|
1a476f7564 | ||
|
|
7f15389c72 | ||
|
|
1a3556e1cc |
2
.github/workflows/ci-build.yaml
vendored
2
.github/workflows/ci-build.yaml
vendored
@@ -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
|
||||
|
||||
50
.github/workflows/release.yaml
vendored
50
.github/workflows/release.yaml
vendored
@@ -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
2
.gitpod.Dockerfile
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
16
cmpserver/plugin/plugin_unix.go
Normal file
16
cmpserver/plugin/plugin_unix.go
Normal 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)
|
||||
}
|
||||
16
cmpserver/plugin/plugin_windows.go
Normal file
16
cmpserver/plugin/plugin_windows.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
101
controller/metrics/workqueue.go
Normal file
101
controller/metrics/workqueue.go
Normal 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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
3
go.mod
@@ -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
1
go.sum
@@ -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=
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
1
manifests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_tmp-bundle
|
||||
5
manifests/addons/README.md
Normal file
5
manifests/addons/README.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
@@ -11,4 +11,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: latest
|
||||
newTag: v2.3.0-rc4
|
||||
|
||||
@@ -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
@@ -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 != "" {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
1
reposerver/repository/testdata/symlinks/bam
vendored
Symbolic link
1
reposerver/repository/testdata/symlinks/bam
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
baz
|
||||
1
reposerver/repository/testdata/symlinks/bar
vendored
Symbolic link
1
reposerver/repository/testdata/symlinks/bar
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
foo
|
||||
1
reposerver/repository/testdata/symlinks/baz
vendored
Symbolic link
1
reposerver/repository/testdata/symlinks/baz
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
bar
|
||||
1
reposerver/repository/testdata/symlinks/foo
vendored
Normal file
1
reposerver/repository/testdata/symlinks/foo
vendored
Normal file
@@ -0,0 +1 @@
|
||||
hello
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
&__external-links-icon-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.filters-group__panel {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status}
|
||||
|
||||
<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}
|
||||
|
||||
<AppUtils.ComparisonStatusIcon status={app.status.sync.status} /> {app.status.sync.status}
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user