mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-25 01:38:46 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6cb4e903f | ||
|
|
710ee9a218 | ||
|
|
e0ff56d89f | ||
|
|
91bb155ed2 | ||
|
|
7ff0ccc34e | ||
|
|
b17323b41e | ||
|
|
1701f7edd3 | ||
|
|
ed5d9c3874 | ||
|
|
14f681e3ee | ||
|
|
8281b18831 | ||
|
|
5bbb51ab42 | ||
|
|
6e181d72b3 | ||
|
|
c415d3f3d5 | ||
|
|
759bd2888d | ||
|
|
2a410f3441 | ||
|
|
689a73a729 | ||
|
|
fbb6b20418 | ||
|
|
f05a6b7906 | ||
|
|
7aac4ba0f0 | ||
|
|
7cac5a8946 | ||
|
|
ba62a0a86d | ||
|
|
3fa66ec42c | ||
|
|
cc672e7609 | ||
|
|
cce1e97434 | ||
|
|
f43122d3cd | ||
|
|
f9436641a6 | ||
|
|
7561cc1cdd |
4
.github/workflows/image-reuse.yaml
vendored
4
.github/workflows/image-reuse.yaml
vendored
@@ -74,9 +74,7 @@ jobs:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2
|
||||
with:
|
||||
cosign-release: 'v2.0.2'
|
||||
uses: sigstore/cosign-installer@e1523de7571e31dbe865fd2e80c5c7c23ae71eb4 # v3.4.0
|
||||
|
||||
- uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
|
||||
- uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
|
||||
|
||||
2
.github/workflows/image.yaml
vendored
2
.github/workflows/image.yaml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
|
||||
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
|
||||
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.7.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
|
||||
with:
|
||||
image: ghcr.io/argoproj/argo-cd/argocd
|
||||
digest: ${{ needs.build-and-publish.outputs.image-digest }}
|
||||
|
||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
|
||||
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
|
||||
with:
|
||||
image: quay.io/argoproj/argocd
|
||||
digest: ${{ needs.argocd-image.outputs.image-digest }}
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
contents: write # Needed for release uploads
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
|
||||
with:
|
||||
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
|
||||
provenance-name: "argocd-cli.intoto.jsonl"
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
contents: write # Needed for release uploads
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
|
||||
with:
|
||||
base64-subjects: "${{ needs.generate-sbom.outputs.hashes }}"
|
||||
provenance-name: "argocd-sbom.intoto.jsonl"
|
||||
|
||||
@@ -6,7 +6,7 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:22.04@sha256:0bced47fffa3361afa981854fca
|
||||
####################################################################################################
|
||||
FROM docker.io/library/golang:1.21.3@sha256:02d7116222536a5cf0fcf631f90b507758b669648e0f20186d2dc94a9b419a9b AS builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
RUN echo 'deb http://archive.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
||||
openssh-server \
|
||||
|
||||
@@ -66,6 +66,7 @@ func NewCommand() *cobra.Command {
|
||||
streamedManifestMaxTarSize string
|
||||
streamedManifestMaxExtractedSize string
|
||||
helmManifestMaxExtractedSize string
|
||||
helmRegistryMaxIndexSize string
|
||||
disableManifestMaxExtractedSize bool
|
||||
)
|
||||
var command = cobra.Command{
|
||||
@@ -108,6 +109,9 @@ func NewCommand() *cobra.Command {
|
||||
helmManifestMaxExtractedSizeQuantity, err := resource.ParseQuantity(helmManifestMaxExtractedSize)
|
||||
errors.CheckError(err)
|
||||
|
||||
helmRegistryMaxIndexSizeQuantity, err := resource.ParseQuantity(helmRegistryMaxIndexSize)
|
||||
errors.CheckError(err)
|
||||
|
||||
askPassServer := askpass.NewServer()
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
@@ -123,6 +127,7 @@ func NewCommand() *cobra.Command {
|
||||
StreamedManifestMaxExtractedSize: streamedManifestMaxExtractedSizeQuantity.ToDec().Value(),
|
||||
StreamedManifestMaxTarSize: streamedManifestMaxTarSizeQuantity.ToDec().Value(),
|
||||
HelmManifestMaxExtractedSize: helmManifestMaxExtractedSizeQuantity.ToDec().Value(),
|
||||
HelmRegistryMaxIndexSize: helmRegistryMaxIndexSizeQuantity.ToDec().Value(),
|
||||
}, askPassServer)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -204,6 +209,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().StringVar(&streamedManifestMaxTarSize, "streamed-manifest-max-tar-size", env.StringFromEnv("ARGOCD_REPO_SERVER_STREAMED_MANIFEST_MAX_TAR_SIZE", "100M"), "Maximum size of streamed manifest archives")
|
||||
command.Flags().StringVar(&streamedManifestMaxExtractedSize, "streamed-manifest-max-extracted-size", env.StringFromEnv("ARGOCD_REPO_SERVER_STREAMED_MANIFEST_MAX_EXTRACTED_SIZE", "1G"), "Maximum size of streamed manifest archives when extracted")
|
||||
command.Flags().StringVar(&helmManifestMaxExtractedSize, "helm-manifest-max-extracted-size", env.StringFromEnv("ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE", "1G"), "Maximum size of helm manifest archives when extracted")
|
||||
command.Flags().StringVar(&helmRegistryMaxIndexSize, "helm-registry-max-index-size", env.StringFromEnv("ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_INDEX_SIZE", "1G"), "Maximum size of registry index file")
|
||||
command.Flags().BoolVar(&disableManifestMaxExtractedSize, "disable-helm-manifest-max-extracted-size", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE", false), "Disable maximum size of helm manifest archives when extracted")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
|
||||
@@ -164,6 +164,11 @@ func NewCommand() *cobra.Command {
|
||||
baseHRef = rootPath
|
||||
}
|
||||
|
||||
var contentTypesList []string
|
||||
if contentTypes != "" {
|
||||
contentTypesList = strings.Split(contentTypes, ";")
|
||||
}
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
ListenPort: listenPort,
|
||||
@@ -179,7 +184,7 @@ func NewCommand() *cobra.Command {
|
||||
DexServerAddr: dexServerAddress,
|
||||
DexTLSConfig: dexTlsConfig,
|
||||
DisableAuth: disableAuth,
|
||||
ContentTypes: strings.Split(contentTypes, ";"),
|
||||
ContentTypes: contentTypesList,
|
||||
EnableGZip: enableGZip,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
@@ -227,7 +232,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", env.StringFromEnv("ARGOCD_SERVER_REPO_SERVER", common.DefaultRepoServerAddr), "Repo server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", env.StringFromEnv("ARGOCD_SERVER_DEX_SERVER", common.DefaultDexServerAddr), "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", env.ParseBoolFromEnv("ARGOCD_SERVER_DISABLE_AUTH", false), "Disable client authentication")
|
||||
command.Flags().StringVar(&contentTypes, "api-content-types", "application/json", "Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty.")
|
||||
command.Flags().StringVar(&contentTypes, "api-content-types", env.StringFromEnv("ARGOCD_API_CONTENT_TYPES", "application/json", env.StringFromEnvOpts{AllowEmpty: true}), "Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty.")
|
||||
command.Flags().BoolVar(&enableGZip, "enable-gzip", env.ParseBoolFromEnv("ARGOCD_SERVER_ENABLE_GZIP", true), "Enable GZIP compression")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.Flags().StringVar(&listenHost, "address", env.StringFromEnv("ARGOCD_SERVER_LISTEN_ADDRESS", common.DefaultAddressAPIServer), "Listen on given address")
|
||||
|
||||
@@ -1031,6 +1031,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
defer argoio.Close(conn)
|
||||
cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
|
||||
diffOption.local = local
|
||||
diffOption.localRepoRoot = localRepoRoot
|
||||
diffOption.cluster = cluster
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -10,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
cdcommon "github.com/argoproj/argo-cd/v2/common"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/kubectl/pkg/util/openapi"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/controller/metrics"
|
||||
@@ -370,11 +371,10 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeTargetResources will apply the diff normalization in all live and target resources.
|
||||
// Then it calculates the merge patch between the normalized live and the current live resources.
|
||||
// Finally it applies the merge patch in the normalized target resources. This is done to ensure
|
||||
// that target resources have the same ignored diff fields values from live ones to avoid them to
|
||||
// be applied in the cluster. Returns the list of normalized target resources.
|
||||
// normalizeTargetResources modifies target resources to ensure ignored fields are not touched during synchronization:
|
||||
// - applies normalization to the target resources based on the live resources
|
||||
// - copies ignored fields from the matching live resources: apply normalizer to the live resource,
|
||||
// calculates the patch performed by normalizer and applies the patch to the target resource
|
||||
func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructured, error) {
|
||||
// normalize live and target resources
|
||||
normalized, err := diff.Normalize(cr.reconciliationResult.Live, cr.reconciliationResult.Target, cr.diffConfig)
|
||||
@@ -393,94 +393,35 @@ func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructure
|
||||
patchedTargets = append(patchedTargets, originalTarget)
|
||||
continue
|
||||
}
|
||||
// calculate targetPatch between normalized and target resource
|
||||
targetPatch, err := getMergePatch(normalizedTarget, originalTarget)
|
||||
|
||||
var lookupPatchMeta *strategicpatch.PatchMetaFromStruct
|
||||
versionedObject, err := scheme.Scheme.New(normalizedTarget.GroupVersionKind())
|
||||
if err == nil {
|
||||
meta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lookupPatchMeta = &meta
|
||||
}
|
||||
|
||||
livePatch, err := getMergePatch(normalized.Lives[idx], live, lookupPatchMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if there is a patch to apply. An empty patch is identified by a '{}' string.
|
||||
if len(targetPatch) > 2 {
|
||||
livePatch, err := getMergePatch(normalized.Lives[idx], live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// generate a minimal patch that uses the fields from targetPatch (template)
|
||||
// with livePatch values
|
||||
patch, err := compilePatch(targetPatch, livePatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalizedTarget, err = applyMergePatch(normalizedTarget, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// if there is no patch just use the original target
|
||||
normalizedTarget = originalTarget
|
||||
normalizedTarget, err = applyMergePatch(normalizedTarget, livePatch, versionedObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patchedTargets = append(patchedTargets, normalizedTarget)
|
||||
}
|
||||
return patchedTargets, nil
|
||||
}
|
||||
|
||||
// compilePatch will generate a patch using the fields from templatePatch with
|
||||
// the values from valuePatch.
|
||||
func compilePatch(templatePatch, valuePatch []byte) ([]byte, error) {
|
||||
templateMap := make(map[string]interface{})
|
||||
err := json.Unmarshal(templatePatch, &templateMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(valuePatch, &valueMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultMap := intersectMap(templateMap, valueMap)
|
||||
return json.Marshal(resultMap)
|
||||
}
|
||||
|
||||
// intersectMap will return map with the fields intersection from the 2 provided
|
||||
// maps populated with the valueMap values.
|
||||
func intersectMap(templateMap, valueMap map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range templateMap {
|
||||
if innerTMap, ok := v.(map[string]interface{}); ok {
|
||||
if innerVMap, ok := valueMap[k].(map[string]interface{}); ok {
|
||||
result[k] = intersectMap(innerTMap, innerVMap)
|
||||
}
|
||||
} else if innerTSlice, ok := v.([]interface{}); ok {
|
||||
if innerVSlice, ok := valueMap[k].([]interface{}); ok {
|
||||
items := []interface{}{}
|
||||
for idx, innerTSliceValue := range innerTSlice {
|
||||
if idx < len(innerVSlice) {
|
||||
if tSliceValueMap, ok := innerTSliceValue.(map[string]interface{}); ok {
|
||||
if vSliceValueMap, ok := innerVSlice[idx].(map[string]interface{}); ok {
|
||||
item := intersectMap(tSliceValueMap, vSliceValueMap)
|
||||
items = append(items, item)
|
||||
}
|
||||
} else {
|
||||
items = append(items, innerVSlice[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
result[k] = items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, ok := valueMap[k]; ok {
|
||||
result[k] = valueMap[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getMergePatch calculates and returns the patch between the original and the
|
||||
// modified unstructures.
|
||||
func getMergePatch(original, modified *unstructured.Unstructured) ([]byte, error) {
|
||||
func getMergePatch(original, modified *unstructured.Unstructured, lookupPatchMeta *strategicpatch.PatchMetaFromStruct) ([]byte, error) {
|
||||
originalJSON, err := original.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -489,20 +430,30 @@ func getMergePatch(original, modified *unstructured.Unstructured) ([]byte, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lookupPatchMeta != nil {
|
||||
return strategicpatch.CreateThreeWayMergePatch(modifiedJSON, modifiedJSON, originalJSON, lookupPatchMeta, true)
|
||||
}
|
||||
|
||||
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
|
||||
}
|
||||
|
||||
// applyMergePatch will apply the given patch in the obj and return the patched
|
||||
// unstructure.
|
||||
func applyMergePatch(obj *unstructured.Unstructured, patch []byte) (*unstructured.Unstructured, error) {
|
||||
func applyMergePatch(obj *unstructured.Unstructured, patch []byte, versionedObject interface{}) (*unstructured.Unstructured, error) {
|
||||
originalJSON, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchedJSON, err := jsonpatch.MergePatch(originalJSON, patch)
|
||||
var patchedJSON []byte
|
||||
if versionedObject == nil {
|
||||
patchedJSON, err = jsonpatch.MergePatch(originalJSON, patch)
|
||||
} else {
|
||||
patchedJSON, err = strategicpatch.StrategicMergePatch(originalJSON, patch, versionedObject)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
patchedObj := &unstructured.Unstructured{}
|
||||
_, _, err = unstructured.UnstructuredJSONScheme.Decode(patchedJSON, nil, patchedObj)
|
||||
if err != nil {
|
||||
|
||||
@@ -386,3 +386,207 @@ func TestNormalizeTargetResources(t *testing.T) {
|
||||
assert.Equal(t, 2, len(containers))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeTargetResourcesWithList(t *testing.T) {
|
||||
type fixture struct {
|
||||
comparisonResult *comparisonResult
|
||||
}
|
||||
setupHttpProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
|
||||
t.Helper()
|
||||
dc, err := diff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(ignores, nil, true).
|
||||
WithNoCache().
|
||||
Build()
|
||||
require.NoError(t, err)
|
||||
live := test.YamlToUnstructured(testdata.LiveHTTPProxy)
|
||||
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
|
||||
return &fixture{
|
||||
&comparisonResult{
|
||||
reconciliationResult: sync.ReconciliationResult{
|
||||
Live: []*unstructured.Unstructured{live},
|
||||
Target: []*unstructured.Unstructured{target},
|
||||
},
|
||||
diffConfig: dc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("will properly ignore nested fields within arrays", func(t *testing.T) {
|
||||
// given
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "projectcontour.io",
|
||||
Kind: "HTTPProxy",
|
||||
JQPathExpressions: []string{".spec.routes[]"},
|
||||
//JSONPointers: []string{"/spec/routes"},
|
||||
},
|
||||
}
|
||||
f := setupHttpProxy(t, ignores)
|
||||
target := test.YamlToUnstructured(testdata.TargetHTTPProxy)
|
||||
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
||||
|
||||
// when
|
||||
patchedTargets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(f.comparisonResult.reconciliationResult.Live))
|
||||
require.Equal(t, 1, len(f.comparisonResult.reconciliationResult.Target))
|
||||
require.Equal(t, 1, len(patchedTargets))
|
||||
|
||||
// live should have 1 entry
|
||||
require.Equal(t, 1, len(dig[[]any](f.comparisonResult.reconciliationResult.Live[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"})))
|
||||
// assert some arbitrary field to show `entries[0]` is not an empty object
|
||||
require.Equal(t, "sample-header", dig[string](f.comparisonResult.reconciliationResult.Live[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeader", "headerName"}))
|
||||
|
||||
// target has 2 entries
|
||||
require.Equal(t, 2, len(dig[[]any](f.comparisonResult.reconciliationResult.Target[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries"})))
|
||||
// assert some arbitrary field to show `entries[0]` is not an empty object
|
||||
require.Equal(t, "sample-header", dig[string](f.comparisonResult.reconciliationResult.Target[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0, "requestHeaderValueMatch", "headers", 0, "name"}))
|
||||
|
||||
// It should be *1* entries in the array
|
||||
require.Equal(t, 1, len(dig[[]any](patchedTargets[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors"})))
|
||||
// and it should NOT equal an empty object
|
||||
require.Len(t, dig[any](patchedTargets[0].Object, []interface{}{"spec", "routes", 0, "rateLimitPolicy", "global", "descriptors", 0, "entries", 0}), 1)
|
||||
|
||||
})
|
||||
t.Run("will correctly set array entries if new entries have been added", func(t *testing.T) {
|
||||
// given
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JQPathExpressions: []string{".spec.template.spec.containers[].env[] | select(.name == \"SOME_ENV_VAR\")"},
|
||||
},
|
||||
}
|
||||
f := setupHttpProxy(t, ignores)
|
||||
live := test.YamlToUnstructured(testdata.LiveDeploymentEnvVarsYaml)
|
||||
target := test.YamlToUnstructured(testdata.TargetDeploymentEnvVarsYaml)
|
||||
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
|
||||
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, len(containers))
|
||||
|
||||
ports := containers[0].(map[string]interface{})["ports"].([]interface{})
|
||||
assert.Equal(t, 1, len(ports))
|
||||
|
||||
env := containers[0].(map[string]interface{})["env"].([]interface{})
|
||||
assert.Equal(t, 3, len(env))
|
||||
|
||||
first := env[0]
|
||||
second := env[1]
|
||||
third := env[2]
|
||||
|
||||
// Currently the defined order at this time is the insertion order of the target manifest.
|
||||
assert.Equal(t, "SOME_ENV_VAR", first.(map[string]interface{})["name"])
|
||||
assert.Equal(t, "some_value", first.(map[string]interface{})["value"])
|
||||
|
||||
assert.Equal(t, "SOME_OTHER_ENV_VAR", second.(map[string]interface{})["name"])
|
||||
assert.Equal(t, "some_other_value", second.(map[string]interface{})["value"])
|
||||
|
||||
assert.Equal(t, "YET_ANOTHER_ENV_VAR", third.(map[string]interface{})["name"])
|
||||
assert.Equal(t, "yet_another_value", third.(map[string]interface{})["value"])
|
||||
})
|
||||
|
||||
t.Run("ignore-deployment-image-replicas-changes-additive", func(t *testing.T) {
|
||||
// given
|
||||
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JSONPointers: []string{"/spec/replicas"},
|
||||
}, {
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JQPathExpressions: []string{".spec.template.spec.containers[].image"},
|
||||
},
|
||||
}
|
||||
f := setupHttpProxy(t, ignores)
|
||||
live := test.YamlToUnstructured(testdata.MinimalImageReplicaDeploymentYaml)
|
||||
target := test.YamlToUnstructured(testdata.AdditionalImageReplicaDeploymentYaml)
|
||||
f.comparisonResult.reconciliationResult.Live = []*unstructured.Unstructured{live}
|
||||
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
metadata, ok, err := unstructured.NestedMap(targets[0].Object, "metadata")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
labels, ok := metadata["labels"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 2, len(labels))
|
||||
assert.Equal(t, "web", labels["appProcess"])
|
||||
|
||||
spec, ok, err := unstructured.NestedMap(targets[0].Object, "spec")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.Equal(t, int64(1), spec["replicas"])
|
||||
|
||||
template, ok := spec["template"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
|
||||
tMetadata, ok := template["metadata"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
tLabels, ok := tMetadata["labels"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 2, len(tLabels))
|
||||
assert.Equal(t, "web", tLabels["appProcess"])
|
||||
|
||||
tSpec, ok := template["spec"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
containers, ok, err := unstructured.NestedSlice(tSpec, "containers")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, len(containers))
|
||||
|
||||
first := containers[0].(map[string]interface{})
|
||||
assert.Equal(t, "alpine:3", first["image"])
|
||||
|
||||
resources, ok := first["resources"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
requests, ok := resources["requests"].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "400m", requests["cpu"])
|
||||
|
||||
env, ok, err := unstructured.NestedSlice(first, "env")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, len(env))
|
||||
|
||||
env0 := env[0].(map[string]interface{})
|
||||
assert.Equal(t, "EV", env0["name"])
|
||||
assert.Equal(t, "here", env0["value"])
|
||||
})
|
||||
}
|
||||
|
||||
func dig[T any](obj interface{}, path []interface{}) T {
|
||||
i := obj
|
||||
|
||||
for _, segment := range path {
|
||||
switch segment.(type) {
|
||||
case int:
|
||||
i = i.([]interface{})[segment.(int)]
|
||||
case string:
|
||||
i = i.(map[string]interface{})[segment.(string)]
|
||||
default:
|
||||
panic("invalid path for object")
|
||||
}
|
||||
}
|
||||
|
||||
return i.(T)
|
||||
}
|
||||
|
||||
28
controller/testdata/additional-image-replicas-deployment.yaml
vendored
Normal file
28
controller/testdata/additional-image-replicas-deployment.yaml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
appProcess: web
|
||||
name: client
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: client
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
appProcess: web
|
||||
spec:
|
||||
containers:
|
||||
- image: alpine:2
|
||||
name: alpine
|
||||
resources:
|
||||
requests:
|
||||
cpu: 400m
|
||||
env:
|
||||
- name: EV
|
||||
value: here
|
||||
18
controller/testdata/data.go
vendored
18
controller/testdata/data.go
vendored
@@ -11,4 +11,22 @@ var (
|
||||
|
||||
//go:embed target-deployment-new-entries.yaml
|
||||
TargetDeploymentNewEntries string
|
||||
|
||||
//go:embed live-httpproxy.yaml
|
||||
LiveHTTPProxy string
|
||||
|
||||
//go:embed target-httpproxy.yaml
|
||||
TargetHTTPProxy string
|
||||
|
||||
//go:embed live-deployment-env-vars.yaml
|
||||
LiveDeploymentEnvVarsYaml string
|
||||
|
||||
//go:embed target-deployment-env-vars.yaml
|
||||
TargetDeploymentEnvVarsYaml string
|
||||
|
||||
//go:embed minimal-image-replicas-deployment.yaml
|
||||
MinimalImageReplicaDeploymentYaml string
|
||||
|
||||
//go:embed additional-image-replicas-deployment.yaml
|
||||
AdditionalImageReplicaDeploymentYaml string
|
||||
)
|
||||
|
||||
177
controller/testdata/live-deployment-env-vars.yaml
vendored
Normal file
177
controller/testdata/live-deployment-env-vars.yaml
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: 'guestbook:apps/Deployment:default/kustomize-guestbook-ui'
|
||||
deployment.kubernetes.io/revision: '9'
|
||||
iksm-version: '2.0'
|
||||
kubectl.kubernetes.io/last-applied-configuration: >
|
||||
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"argocd.argoproj.io/tracking-id":"guestbook:apps/Deployment:default/kustomize-guestbook-ui","iksm-version":"2.0"},"name":"kustomize-guestbook-ui","namespace":"default"},"spec":{"replicas":4,"revisionHistoryLimit":3,"selector":{"matchLabels":{"app":"guestbook-ui"}},"template":{"metadata":{"labels":{"app":"guestbook-ui"}},"spec":{"containers":[{"env":[{"name":"SOME_ENV_VAR","value":"some_value"}],"image":"gcr.io/heptio-images/ks-guestbook-demo:0.1","name":"guestbook-ui","ports":[{"containerPort":80}],"resources":{"requests":{"cpu":"50m","memory":"100Mi"}}}]}}}}
|
||||
creationTimestamp: '2022-01-05T15:45:21Z'
|
||||
generation: 119
|
||||
managedFields:
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
'f:iksm-version': {}
|
||||
manager: janitor
|
||||
operation: Apply
|
||||
time: '2022-01-06T18:21:04Z'
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
.: {}
|
||||
'f:argocd.argoproj.io/tracking-id': {}
|
||||
'f:kubectl.kubernetes.io/last-applied-configuration': {}
|
||||
'f:spec':
|
||||
'f:progressDeadlineSeconds': {}
|
||||
'f:replicas': {}
|
||||
'f:revisionHistoryLimit': {}
|
||||
'f:selector': {}
|
||||
'f:strategy':
|
||||
'f:rollingUpdate':
|
||||
.: {}
|
||||
'f:maxSurge': {}
|
||||
'f:maxUnavailable': {}
|
||||
'f:type': {}
|
||||
'f:template':
|
||||
'f:metadata':
|
||||
'f:labels':
|
||||
.: {}
|
||||
'f:app': {}
|
||||
'f:spec':
|
||||
'f:containers':
|
||||
'k:{"name":"guestbook-ui"}':
|
||||
.: {}
|
||||
'f:env':
|
||||
.: {}
|
||||
'k:{"name":"SOME_ENV_VAR"}':
|
||||
.: {}
|
||||
'f:name': {}
|
||||
'f:value': {}
|
||||
'f:image': {}
|
||||
'f:imagePullPolicy': {}
|
||||
'f:name': {}
|
||||
'f:ports':
|
||||
.: {}
|
||||
'k:{"containerPort":80,"protocol":"TCP"}':
|
||||
.: {}
|
||||
'f:containerPort': {}
|
||||
'f:protocol': {}
|
||||
'f:resources':
|
||||
.: {}
|
||||
'f:requests':
|
||||
.: {}
|
||||
'f:cpu': {}
|
||||
'f:memory': {}
|
||||
'f:terminationMessagePath': {}
|
||||
'f:terminationMessagePolicy': {}
|
||||
'f:dnsPolicy': {}
|
||||
'f:restartPolicy': {}
|
||||
'f:schedulerName': {}
|
||||
'f:securityContext': {}
|
||||
'f:terminationGracePeriodSeconds': {}
|
||||
manager: argocd
|
||||
operation: Update
|
||||
time: '2022-01-06T15:04:15Z'
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
'f:deployment.kubernetes.io/revision': {}
|
||||
'f:status':
|
||||
'f:availableReplicas': {}
|
||||
'f:conditions':
|
||||
.: {}
|
||||
'k:{"type":"Available"}':
|
||||
.: {}
|
||||
'f:lastTransitionTime': {}
|
||||
'f:lastUpdateTime': {}
|
||||
'f:message': {}
|
||||
'f:reason': {}
|
||||
'f:status': {}
|
||||
'f:type': {}
|
||||
'k:{"type":"Progressing"}':
|
||||
.: {}
|
||||
'f:lastTransitionTime': {}
|
||||
'f:lastUpdateTime': {}
|
||||
'f:message': {}
|
||||
'f:reason': {}
|
||||
'f:status': {}
|
||||
'f:type': {}
|
||||
'f:observedGeneration': {}
|
||||
'f:readyReplicas': {}
|
||||
'f:replicas': {}
|
||||
'f:updatedReplicas': {}
|
||||
manager: kube-controller-manager
|
||||
operation: Update
|
||||
time: '2022-01-06T18:15:14Z'
|
||||
name: kustomize-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: '8289211'
|
||||
uid: ef253575-ce44-4c5e-84ad-16e81d0df6eb
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 4
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: guestbook-ui
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: SOME_ENV_VAR
|
||||
value: some_value
|
||||
image: 'gcr.io/heptio-images/ks-guestbook-demo:0.1'
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: guestbook-ui
|
||||
ports:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 100Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
status:
|
||||
availableReplicas: 4
|
||||
conditions:
|
||||
- lastTransitionTime: '2022-01-05T22:20:37Z'
|
||||
lastUpdateTime: '2022-01-05T22:43:47Z'
|
||||
message: >-
|
||||
ReplicaSet "kustomize-guestbook-ui-6549d54677" has successfully
|
||||
progressed.
|
||||
reason: NewReplicaSetAvailable
|
||||
status: 'True'
|
||||
type: Progressing
|
||||
- lastTransitionTime: '2022-01-06T18:15:14Z'
|
||||
lastUpdateTime: '2022-01-06T18:15:14Z'
|
||||
message: Deployment has minimum availability.
|
||||
reason: MinimumReplicasAvailable
|
||||
status: 'True'
|
||||
type: Available
|
||||
observedGeneration: 119
|
||||
readyReplicas: 4
|
||||
replicas: 4
|
||||
updatedReplicas: 4
|
||||
14
controller/testdata/live-httpproxy.yaml
vendored
Normal file
14
controller/testdata/live-httpproxy.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
name: my-http-proxy
|
||||
namespace: default
|
||||
spec:
|
||||
routes:
|
||||
- rateLimitPolicy:
|
||||
global:
|
||||
descriptors:
|
||||
- entries:
|
||||
- requestHeader:
|
||||
descriptorKey: sample-key
|
||||
headerName: sample-header
|
||||
21
controller/testdata/minimal-image-replicas-deployment.yaml
vendored
Normal file
21
controller/testdata/minimal-image-replicas-deployment.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
name: client
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: client
|
||||
strategy: {}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
spec:
|
||||
containers:
|
||||
- image: alpine:3
|
||||
name: alpine
|
||||
resources: {}
|
||||
35
controller/testdata/target-deployment-env-vars.yaml
vendored
Normal file
35
controller/testdata/target-deployment-env-vars.yaml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: 'guestbook:apps/Deployment:default/kustomize-guestbook-ui'
|
||||
iksm-version: '1.0'
|
||||
name: kustomize-guestbook-ui
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: guestbook-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: SOME_OTHER_ENV_VAR
|
||||
value: some_other_value
|
||||
- name: YET_ANOTHER_ENV_VAR
|
||||
value: yet_another_value
|
||||
- name: SOME_ENV_VAR
|
||||
value: different_value!
|
||||
image: 'gcr.io/heptio-images/ks-guestbook-demo:0.1'
|
||||
name: guestbook-ui
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 100Mi
|
||||
23
controller/testdata/target-httpproxy.yaml
vendored
Normal file
23
controller/testdata/target-httpproxy.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: projectcontour.io/v1
|
||||
kind: HTTPProxy
|
||||
metadata:
|
||||
name: my-http-proxy
|
||||
namespace: default
|
||||
spec:
|
||||
routes:
|
||||
- rateLimitPolicy:
|
||||
global:
|
||||
descriptors:
|
||||
- entries:
|
||||
- requestHeaderValueMatch:
|
||||
headers:
|
||||
- contains: sample-key
|
||||
name: sample-header
|
||||
value: third
|
||||
- requestHeader:
|
||||
descriptorKey: sample-key
|
||||
headerName: sample-header
|
||||
- entries:
|
||||
- requestHeader:
|
||||
descriptorKey: sample-key
|
||||
headerName: sample-header
|
||||
@@ -72,6 +72,9 @@ data:
|
||||
server.rootpath: ""
|
||||
# Directory path that contains additional static assets
|
||||
server.staticassets: "/shared/app"
|
||||
# Semicolon-separated list of content types allowed on non-GET requests. Set an empty string to allow all. Be aware
|
||||
# that allowing content types besides application/json may make your API more vulnerable to CSRF attacks.
|
||||
server.api.content.types: "application/json"
|
||||
|
||||
# Set the logging format. One of: text|json (default "text")
|
||||
server.log.format: "text"
|
||||
|
||||
5
go.mod
5
go.mod
@@ -26,6 +26,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/gfleury/go-bitbucket-v1 v0.0.0-20220301131131-8e7ed04b843e
|
||||
github.com/go-git/go-git/v5 v5.11.0
|
||||
github.com/go-jose/go-jose/v3 v3.0.1
|
||||
github.com/go-logr/logr v1.2.4
|
||||
github.com/go-openapi/loads v0.21.2
|
||||
github.com/go-openapi/runtime v0.26.0
|
||||
@@ -85,7 +86,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc
|
||||
google.golang.org/grpc v1.56.2
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.24.17
|
||||
@@ -175,7 +175,6 @@ require (
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.5.0 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.21.4 // indirect
|
||||
github.com/go-openapi/errors v0.20.3 // indirect
|
||||
@@ -233,7 +232,7 @@ require (
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.10.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
|
||||
6
go.sum
6
go.sum
@@ -931,8 +931,8 @@ github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lK
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
|
||||
github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
|
||||
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
@@ -2612,8 +2612,6 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs=
|
||||
gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
|
||||
@@ -5,7 +5,7 @@ kind: Kustomization
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.4
|
||||
newTag: v2.9.12
|
||||
resources:
|
||||
- ./application-controller
|
||||
- ./dex
|
||||
|
||||
@@ -25,136 +25,136 @@ spec:
|
||||
env:
|
||||
- name: ARGOCD_SERVER_INSECURE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.insecure
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.insecure
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_BASEHREF
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.basehref
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.basehref
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ROOTPATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.rootpath
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.rootpath
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOGFORMAT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.format
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.format
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOG_LEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.level
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.log.level
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: repo.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: repo.server
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DISABLE_AUTH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.disable.auth
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.disable.auth
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ENABLE_GZIP
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.gzip
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.gzip
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.timeout.seconds
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.timeout.seconds
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_X_FRAME_OPTIONS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.x.frame.options
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.x.frame.options
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.content.security.policy
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.content.security.policy
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.plaintext
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.plaintext
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_REPO_SERVER_STRICT_TLS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.strict.tls
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.repo.server.strict.tls
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER_PLAINTEXT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.plaintext
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.plaintext
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_DEX_SERVER_STRICT_TLS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.strict.tls
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.dex.server.strict.tls
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_MIN_VERSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.minversion
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.minversion
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_MAX_VERSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.maxversion
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.maxversion
|
||||
optional: true
|
||||
- name: ARGOCD_TLS_CIPHERS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.ciphers
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.tls.ciphers
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_CONNECTION_STATUS_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.connection.status.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.connection.status.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OIDC_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.oidc.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.oidc.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LOGIN_ATTEMPTS_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.login.attempts.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.login.attempts.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_STATIC_ASSETS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -163,16 +163,16 @@ spec:
|
||||
optional: true
|
||||
- name: ARGOCD_APP_STATE_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.app.state.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.app.state.cache.expiration
|
||||
optional: true
|
||||
- name: REDIS_SERVER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.server
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.server
|
||||
optional: true
|
||||
- name: REDIS_COMPRESSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -181,52 +181,58 @@ spec:
|
||||
optional: true
|
||||
- name: REDISDB
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.db
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: redis.db
|
||||
optional: true
|
||||
- name: ARGOCD_DEFAULT_CACHE_EXPIRATION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.default.cache.expiration
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.default.cache.expiration
|
||||
optional: true
|
||||
- name: ARGOCD_MAX_COOKIE_NUMBER
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.http.cookie.maxnumber
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.http.cookie.maxnumber
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.listen.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_METRICS_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.metrics.listen.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.metrics.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.address
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.address
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: application.namespaces
|
||||
optional: true
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: application.namespaces
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_ENABLE_PROXY_EXTENSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.enable.proxy.extension
|
||||
optional: true
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: server.api.content.types
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: ssh-known-hosts
|
||||
mountPath: /app/config/ssh
|
||||
|
||||
@@ -20750,7 +20750,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -21050,7 +21050,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -21102,7 +21102,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -21321,7 +21321,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.4
|
||||
newTag: v2.9.12
|
||||
|
||||
@@ -12,7 +12,7 @@ patches:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: v2.9.4
|
||||
newTag: v2.9.12
|
||||
resources:
|
||||
- ../../base/application-controller
|
||||
- ../../base/applicationset-controller
|
||||
|
||||
@@ -22007,7 +22007,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -22130,7 +22130,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -22206,7 +22206,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -22537,7 +22537,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -22589,7 +22589,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -22878,7 +22878,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -23124,7 +23130,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -1662,7 +1662,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -1785,7 +1785,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -1861,7 +1861,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -2192,7 +2192,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -2244,7 +2244,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -2533,7 +2533,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -2779,7 +2785,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -21102,7 +21102,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -21225,7 +21225,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -21301,7 +21301,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -21583,7 +21583,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -21635,7 +21635,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -21922,7 +21922,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -22168,7 +22174,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -757,7 +757,7 @@ spec:
|
||||
key: applicationsetcontroller.allowed.scm.providers
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-applicationset-controller
|
||||
ports:
|
||||
@@ -880,7 +880,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /shared/argocd-dex
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: copyutil
|
||||
securityContext:
|
||||
@@ -956,7 +956,7 @@ spec:
|
||||
key: application.namespaces
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
@@ -1238,7 +1238,7 @@ spec:
|
||||
value: /helm-working-dir
|
||||
- name: HELM_DATA_HOME
|
||||
value: /helm-working-dir
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
@@ -1290,7 +1290,7 @@ spec:
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
name: copyutil
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -1577,7 +1577,13 @@ spec:
|
||||
key: server.enable.proxy.extension
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
- name: ARGOCD_API_CONTENT_TYPES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: server.api.content.types
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
@@ -1823,7 +1829,7 @@ spec:
|
||||
key: controller.kubectl.parallelism.limit
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:v2.9.4
|
||||
image: quay.io/argoproj/argocd:v2.9.12
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
|
||||
@@ -17,6 +17,24 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type ErrApplicationNotAllowedToUseProject struct {
|
||||
application string
|
||||
namespace string
|
||||
project string
|
||||
}
|
||||
|
||||
func NewErrApplicationNotAllowedToUseProject(application, namespace, project string) error {
|
||||
return &ErrApplicationNotAllowedToUseProject{
|
||||
application: application,
|
||||
namespace: namespace,
|
||||
project: project,
|
||||
}
|
||||
}
|
||||
|
||||
func (err *ErrApplicationNotAllowedToUseProject) Error() string {
|
||||
return fmt.Sprintf("application '%s' in namespace '%s' is not allowed to use project %s", err.application, err.namespace, err.project)
|
||||
}
|
||||
|
||||
// AppProjectList is list of AppProject resources
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type AppProjectList struct {
|
||||
|
||||
@@ -107,6 +107,7 @@ type RepoServerInitConstants struct {
|
||||
StreamedManifestMaxExtractedSize int64
|
||||
StreamedManifestMaxTarSize int64
|
||||
HelmManifestMaxExtractedSize int64
|
||||
HelmRegistryMaxIndexSize int64
|
||||
DisableHelmManifestMaxExtractedSize bool
|
||||
}
|
||||
|
||||
@@ -1378,7 +1379,7 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string,
|
||||
if q.KustomizeOptions != nil {
|
||||
kustomizeBinary = q.KustomizeOptions.BinaryPath
|
||||
}
|
||||
k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary)
|
||||
k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary)
|
||||
targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions, env)
|
||||
case v1alpha1.ApplicationSourceTypePlugin:
|
||||
pluginName := ""
|
||||
@@ -1965,7 +1966,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
|
||||
return err
|
||||
}
|
||||
case v1alpha1.ApplicationSourceTypeKustomize:
|
||||
if err := populateKustomizeAppDetails(res, q, opContext.appPath, commitSHA, s.gitCredsStore); err != nil {
|
||||
if err := populateKustomizeAppDetails(res, q, repoRoot, opContext.appPath, commitSHA, s.gitCredsStore); err != nil {
|
||||
return err
|
||||
}
|
||||
case v1alpha1.ApplicationSourceTypePlugin:
|
||||
@@ -2106,13 +2107,13 @@ func findHelmValueFilesInPath(path string) ([]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, appPath string, reversion string, credsStore git.CredsStore) error {
|
||||
func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, repoRoot string, appPath string, reversion string, credsStore git.CredsStore) error {
|
||||
res.Kustomize = &apiclient.KustomizeAppSpec{}
|
||||
kustomizeBinary := ""
|
||||
if q.KustomizeOptions != nil {
|
||||
kustomizeBinary = q.KustomizeOptions.BinaryPath
|
||||
}
|
||||
k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary)
|
||||
k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary)
|
||||
fakeManifestRequest := apiclient.ManifestRequest{
|
||||
AppName: q.AppName,
|
||||
Namespace: "", // FIXME: omit it for now
|
||||
@@ -2345,7 +2346,7 @@ func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revisi
|
||||
return helmClient, version.String(), nil
|
||||
}
|
||||
|
||||
index, err := helmClient.GetIndex(noRevisionCache)
|
||||
index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -2423,7 +2424,7 @@ func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bo
|
||||
}
|
||||
|
||||
func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {
|
||||
index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI, q.Repo.Proxy, helm.WithChartPaths(s.chartPaths)).GetIndex(true)
|
||||
index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI, q.Repo.Proxy, helm.WithChartPaths(s.chartPaths)).GetIndex(true, s.initConstants.HelmRegistryMaxIndexSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2458,7 +2459,7 @@ func (s *Service) TestRepository(ctx context.Context, q *apiclient.TestRepositor
|
||||
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy).TestHelmOCI()
|
||||
return err
|
||||
} else {
|
||||
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy).GetIndex(false)
|
||||
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy).GetIndex(false, s.initConstants.HelmRegistryMaxIndexSize)
|
||||
return err
|
||||
}
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
|
||||
chart := "my-chart"
|
||||
oobChart := "out-of-bounds-chart"
|
||||
version := "1.1.0"
|
||||
helmClient.On("GetIndex", mock.AnythingOfType("bool")).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
helmClient.On("GetIndex", mock.AnythingOfType("bool"), mock.Anything).Return(&helm.Index{Entries: map[string]helm.Entries{
|
||||
chart: {{Version: "1.0.0"}, {Version: version}},
|
||||
oobChart: {{Version: "1.0.0"}, {Version: version}},
|
||||
}}, nil)
|
||||
|
||||
@@ -148,7 +148,7 @@ func NewServer(
|
||||
//
|
||||
// If the user does provide a "project," we can respond more specifically. If the user does not have access to the given
|
||||
// app name in the given project, we return "permission denied." If the app exists, but the project is different from
|
||||
func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) {
|
||||
func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, *appv1.AppProject, error) {
|
||||
logCtx := log.WithFields(map[string]interface{}{
|
||||
"application": name,
|
||||
"namespace": namespace,
|
||||
@@ -165,7 +165,7 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa
|
||||
// but the app is in a different project" response. We don't want the user inferring the existence of the
|
||||
// app from response time.
|
||||
_, _ = getApp()
|
||||
return nil, permissionDeniedErr
|
||||
return nil, nil, permissionDeniedErr
|
||||
}
|
||||
}
|
||||
a, err := getApp()
|
||||
@@ -173,15 +173,15 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa
|
||||
if apierr.IsNotFound(err) {
|
||||
if project != "" {
|
||||
// We know that the user was allowed to get the Application, but the Application does not exist. Return 404.
|
||||
return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
}
|
||||
// We don't know if the user was allowed to get the Application, and we don't want to leak information about
|
||||
// the Application's existence. Return 403.
|
||||
logCtx.Warn("application does not exist")
|
||||
return nil, permissionDeniedErr
|
||||
return nil, nil, permissionDeniedErr
|
||||
}
|
||||
logCtx.Errorf("failed to get application: %s", err)
|
||||
return nil, permissionDeniedErr
|
||||
return nil, nil, permissionDeniedErr
|
||||
}
|
||||
// Even if we performed an initial RBAC check (because the request was fully parameterized), we still need to
|
||||
// perform a second RBAC check to ensure that the user has access to the actual Application's project (not just the
|
||||
@@ -195,11 +195,11 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa
|
||||
// The user specified a project. We would have returned a 404 if the user had access to the app, but the app
|
||||
// did not exist. So we have to return a 404 when the app does exist, but the user does not have access.
|
||||
// Otherwise, they could infer that the app exists based on the error code.
|
||||
return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
}
|
||||
// The user didn't specify a project. We always return permission denied for both lack of access and lack of
|
||||
// existence.
|
||||
return nil, permissionDeniedErr
|
||||
return nil, nil, permissionDeniedErr
|
||||
}
|
||||
effectiveProject := "default"
|
||||
if a.Spec.Project != "" {
|
||||
@@ -212,15 +212,20 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa
|
||||
}).Warnf("user tried to %s application in project %s, but the application is in project %s", action, project, effectiveProject)
|
||||
// The user has access to the app, but the app is in a different project. Return 404, meaning "app doesn't
|
||||
// exist in that project".
|
||||
return nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
return nil, nil, status.Errorf(codes.NotFound, apierr.NewNotFound(schema.GroupResource{Group: "argoproj.io", Resource: "applications"}, name).Error())
|
||||
}
|
||||
return a, nil
|
||||
// Get the app's associated project, and make sure all project restrictions are enforced.
|
||||
proj, err := s.getAppProject(ctx, a, logCtx)
|
||||
if err != nil {
|
||||
return a, nil, err
|
||||
}
|
||||
return a, proj, nil
|
||||
}
|
||||
|
||||
// getApplicationEnforceRBACInformer uses an informer to get an Application. If the app does not exist, permission is
|
||||
// denied, or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive
|
||||
// information.
|
||||
func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, error) {
|
||||
func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*appv1.Application, *appv1.AppProject, error) {
|
||||
namespaceOrDefault := s.appNamespaceOrDefault(namespace)
|
||||
return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) {
|
||||
return s.appLister.Applications(namespaceOrDefault).Get(name)
|
||||
@@ -230,7 +235,7 @@ func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action,
|
||||
// getApplicationEnforceRBACClient uses a client to get an Application. If the app does not exist, permission is denied,
|
||||
// or any other error occurs when getting the app, we return a permission denied error to obscure any sensitive
|
||||
// information.
|
||||
func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, error) {
|
||||
func (s *Server) getApplicationEnforceRBACClient(ctx context.Context, action, project, namespace, name, resourceVersion string) (*appv1.Application, *appv1.AppProject, error) {
|
||||
namespaceOrDefault := s.appNamespaceOrDefault(namespace)
|
||||
return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*appv1.Application, error) {
|
||||
if !s.isNamespaceEnabled(namespaceOrDefault) {
|
||||
@@ -314,7 +319,13 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq
|
||||
if q.Validate != nil {
|
||||
validate = *q.Validate
|
||||
}
|
||||
err := s.validateAndNormalizeApp(ctx, a, validate)
|
||||
|
||||
proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.validateAndNormalizeApp(ctx, a, proj, validate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while validating and normalizing app: %w", err)
|
||||
}
|
||||
@@ -325,6 +336,15 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq
|
||||
return nil, security.NamespaceNotPermittedError(appNs)
|
||||
}
|
||||
|
||||
// Don't let the app creator set the operation explicitly. Those requests should always go through the Sync API.
|
||||
if a.Operation != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"application": a.Name,
|
||||
argocommon.SecurityField: argocommon.SecurityLow,
|
||||
}).Warn("User attempted to set operation on application creation. This could have allowed them to bypass branch protection rules by setting manifests directly. Ignoring the set operation.")
|
||||
a.Operation = nil
|
||||
}
|
||||
|
||||
created, err := s.appclientset.ArgoprojV1alpha1().Applications(appNs).Create(ctx, a, metav1.CreateOptions{})
|
||||
if err == nil {
|
||||
s.logAppEvent(created, ctx, argo.EventReasonResourceCreated, "created application")
|
||||
@@ -361,7 +381,7 @@ func (s *Server) Create(ctx context.Context, q *application.ApplicationCreateReq
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, action func(
|
||||
func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, proj *appv1.AppProject, action func(
|
||||
client apiclient.RepoServerServiceClient,
|
||||
repo *appv1.Repository,
|
||||
helmRepos []*appv1.Repository,
|
||||
@@ -388,13 +408,6 @@ func (s *Server) queryRepoServer(ctx context.Context, a *appv1.Application, acti
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting kustomize settings options: %w", err)
|
||||
}
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project)
|
||||
}
|
||||
return fmt.Errorf("error getting application's project: %w", err)
|
||||
}
|
||||
|
||||
helmRepos, err := s.db.ListHelmRepositories(ctx)
|
||||
if err != nil {
|
||||
@@ -429,7 +442,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan
|
||||
if q.Name == nil || *q.Name == "" {
|
||||
return nil, fmt.Errorf("invalid request: application name is missing")
|
||||
}
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -441,7 +454,7 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan
|
||||
}
|
||||
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
err = s.queryRepoServer(ctx, a, func(
|
||||
err = s.queryRepoServer(ctx, a, proj, func(
|
||||
client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error {
|
||||
revision := source.TargetRevision
|
||||
if q.GetRevision() != "" {
|
||||
@@ -467,11 +480,6 @@ func (s *Server) GetManifests(ctx context.Context, q *application.ApplicationMan
|
||||
return fmt.Errorf("error getting API resources: %w", err)
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting app project: %w", err)
|
||||
}
|
||||
|
||||
manifestInfo, err = client.GenerateManifest(ctx, &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Revision: revision,
|
||||
@@ -534,13 +542,13 @@ func (s *Server) GetManifestsWithFiles(stream application.ApplicationService_Get
|
||||
return fmt.Errorf("invalid request: application name is missing")
|
||||
}
|
||||
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName())
|
||||
a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, query.GetProject(), query.GetAppNamespace(), query.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
err = s.queryRepoServer(ctx, a, func(
|
||||
err = s.queryRepoServer(ctx, a, proj, func(
|
||||
client apiclient.RepoServerServiceClient, repo *appv1.Repository, helmRepos []*appv1.Repository, helmCreds []*appv1.RepoCreds, helmOptions *appv1.HelmOptions, kustomizeOptions *appv1.KustomizeOptions, enableGenerateManifests map[string]bool) error {
|
||||
|
||||
appInstanceLabelKey, err := s.settingsMgr.GetAppInstanceLabelKey()
|
||||
@@ -651,7 +659,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app
|
||||
// We must use a client Get instead of an informer Get, because it's common to call Get immediately
|
||||
// following a Watch (which is not yet powered by an informer), and the Get must reflect what was
|
||||
// previously seen by the client.
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion())
|
||||
a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, project, appNs, appName, q.GetResourceVersion())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -682,7 +690,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app
|
||||
|
||||
if refreshType == appv1.RefreshTypeHard {
|
||||
// force refresh cached application details
|
||||
if err := s.queryRepoServer(ctx, a, func(
|
||||
if err := s.queryRepoServer(ctx, a, proj, func(
|
||||
client apiclient.RepoServerServiceClient,
|
||||
repo *appv1.Repository,
|
||||
helmRepos []*appv1.Repository,
|
||||
@@ -734,7 +742,7 @@ func (s *Server) Get(ctx context.Context, q *application.ApplicationQuery) (*app
|
||||
|
||||
// ListResourceEvents returns a list of event resources
|
||||
func (s *Server) ListResourceEvents(ctx context.Context, q *application.ApplicationResourceEventsQuery) (*v1.EventList, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -802,12 +810,12 @@ func (s *Server) validateAndUpdateApp(ctx context.Context, newApp *appv1.Applica
|
||||
s.projectLock.RLock(newApp.Spec.GetProject())
|
||||
defer s.projectLock.RUnlock(newApp.Spec.GetProject())
|
||||
|
||||
app, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "")
|
||||
app, proj, err := s.getApplicationEnforceRBACClient(ctx, action, currentProject, newApp.Namespace, newApp.Name, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.validateAndNormalizeApp(ctx, newApp, validate)
|
||||
err = s.validateAndNormalizeApp(ctx, newApp, proj, validate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error validating and normalizing app: %w", err)
|
||||
}
|
||||
@@ -906,7 +914,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat
|
||||
if q.GetSpec() == nil {
|
||||
return nil, fmt.Errorf("error updating application spec: spec is nil in request")
|
||||
}
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionUpdate, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -925,7 +933,7 @@ func (s *Server) UpdateSpec(ctx context.Context, q *application.ApplicationUpdat
|
||||
|
||||
// Patch patches an application
|
||||
func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchRequest) (*appv1.Application, error) {
|
||||
app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
app, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -968,11 +976,35 @@ func (s *Server) Patch(ctx context.Context, q *application.ApplicationPatchReque
|
||||
return s.validateAndUpdateApp(ctx, newApp, false, true, rbacpolicy.ActionUpdate, q.GetProject())
|
||||
}
|
||||
|
||||
func (s *Server) getAppProject(ctx context.Context, a *appv1.Application, logCtx *log.Entry) (*appv1.AppProject, error) {
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err == nil {
|
||||
return proj, nil
|
||||
}
|
||||
|
||||
// If there's a permission issue or the app doesn't exist, return a vague error to avoid letting the user enumerate project names.
|
||||
vagueError := status.Errorf(codes.InvalidArgument, "app is not allowed in project %q, or the project does not exist", a.Spec.Project)
|
||||
|
||||
if apierr.IsNotFound(err) {
|
||||
return nil, vagueError
|
||||
}
|
||||
|
||||
if _, ok := err.(*appv1.ErrApplicationNotAllowedToUseProject); ok {
|
||||
logCtx.WithFields(map[string]interface{}{
|
||||
"project": a.Spec.Project,
|
||||
argocommon.SecurityField: argocommon.SecurityMedium,
|
||||
}).Warnf("error getting app project: %s", err)
|
||||
return nil, vagueError
|
||||
}
|
||||
|
||||
return nil, vagueError
|
||||
}
|
||||
|
||||
// Delete removes an application and all associated resources
|
||||
func (s *Server) Delete(ctx context.Context, q *application.ApplicationDeleteRequest) (*application.ApplicationResponse, error) {
|
||||
appName := q.GetName()
|
||||
appNs := s.appNamespaceOrDefault(q.GetAppNamespace())
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "")
|
||||
a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), appNs, appName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1127,16 +1159,7 @@ func (s *Server) Watch(q *application.ApplicationQuery, ws application.Applicati
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, validate bool) error {
|
||||
proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
// Offer no hint that the project does not exist.
|
||||
log.Warnf("User attempted to create/update application in non-existent project %q", app.Spec.Project)
|
||||
return permissionDeniedErr
|
||||
}
|
||||
return fmt.Errorf("error getting application's project: %w", err)
|
||||
}
|
||||
func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, validate bool) error {
|
||||
if app.GetName() == "" {
|
||||
return fmt.Errorf("resource name may not be empty")
|
||||
}
|
||||
@@ -1240,7 +1263,7 @@ func (s *Server) getAppResources(ctx context.Context, a *appv1.Application) (*ap
|
||||
}
|
||||
|
||||
func (s *Server) getAppLiveResource(ctx context.Context, action string, q *application.ApplicationResourceRequest) (*appv1.ResourceNode, *rest.Config, *appv1.Application, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, action, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -1377,7 +1400,7 @@ func (s *Server) DeleteResource(ctx context.Context, q *application.ApplicationR
|
||||
}
|
||||
|
||||
func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery) (*appv1.ApplicationTree, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1386,7 +1409,7 @@ func (s *Server) ResourceTree(ctx context.Context, q *application.ResourcesQuery
|
||||
}
|
||||
|
||||
func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application.ApplicationService_WatchResourceTreeServer) error {
|
||||
_, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
_, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1403,7 +1426,7 @@ func (s *Server) WatchResourceTree(q *application.ResourcesQuery, ws application
|
||||
}
|
||||
|
||||
func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.RevisionMetadata, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, proj, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1413,12 +1436,6 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting repository by URL: %w", err)
|
||||
}
|
||||
// We need to get some information with the project associated to the app,
|
||||
// so we'll know whether GPG signatures are enforced.
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting app project: %w", err)
|
||||
}
|
||||
conn, repoClient, err := s.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating repo server client: %w", err)
|
||||
@@ -1433,7 +1450,7 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
|
||||
|
||||
// RevisionChartDetails returns the helm chart metadata, as fetched from the reposerver
|
||||
func (s *Server) RevisionChartDetails(ctx context.Context, q *application.RevisionMetadataQuery) (*appv1.ChartDetails, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1464,7 +1481,7 @@ func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) boo
|
||||
}
|
||||
|
||||
func (s *Server) ManagedResources(ctx context.Context, q *application.ResourcesQuery) (*application.ManagedResourcesResponse, error) {
|
||||
a, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetApplicationName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1521,7 +1538,7 @@ func (s *Server) PodLogs(q *application.ApplicationPodLogsQuery, ws application.
|
||||
}
|
||||
}
|
||||
|
||||
a, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ws.Context(), rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1713,19 +1730,11 @@ func isTheSelectedOne(currentNode *appv1.ResourceNode, q *application.Applicatio
|
||||
|
||||
// Sync syncs an application to its target state
|
||||
func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncRequest) (*appv1.Application, error) {
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "")
|
||||
a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, syncReq.GetProject(), syncReq.GetAppNamespace(), syncReq.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return a, status.Errorf(codes.InvalidArgument, "application references project %s which does not exist", a.Spec.Project)
|
||||
}
|
||||
return a, fmt.Errorf("error getting app project: %w", err)
|
||||
}
|
||||
|
||||
s.inferResourcesStatusHealth(a)
|
||||
|
||||
if !proj.Spec.SyncWindows.Matches(a).CanSync(true) {
|
||||
@@ -1822,7 +1831,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR
|
||||
}
|
||||
|
||||
func (s *Server) Rollback(ctx context.Context, rollbackReq *application.ApplicationRollbackRequest) (*appv1.Application, error) {
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "")
|
||||
a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, rollbackReq.GetProject(), rollbackReq.GetAppNamespace(), rollbackReq.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1881,7 +1890,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *application.Applicat
|
||||
}
|
||||
|
||||
func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksRequest) (*application.LinksResponse, error) {
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "")
|
||||
a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, req.GetProject(), req.GetNamespace(), req.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1896,7 +1905,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq
|
||||
return nil, fmt.Errorf("failed to read application deep links from configmap: %w", err)
|
||||
}
|
||||
|
||||
clstObj, _, err := s.getObjectsForDeepLinks(ctx, a)
|
||||
clstObj, _, err := s.getObjectsForDeepLinks(ctx, a, proj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1911,12 +1920,7 @@ func (s *Server) ListLinks(ctx context.Context, req *application.ListAppLinksReq
|
||||
return finalList, nil
|
||||
}
|
||||
|
||||
func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) {
|
||||
proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error getting app project: %w", err)
|
||||
}
|
||||
|
||||
func (s *Server) getObjectsForDeepLinks(ctx context.Context, app *appv1.Application, proj *appv1.AppProject) (cluster *unstructured.Unstructured, project *unstructured.Unstructured, err error) {
|
||||
// sanitize project jwt tokens
|
||||
proj.Status = appv1.AppProjectStatus{}
|
||||
|
||||
@@ -1979,7 +1983,12 @@ func (s *Server) ListResourceLinks(ctx context.Context, req *application.Applica
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app)
|
||||
proj, err := s.getAppProject(ctx, app, log.WithField("application", app.GetName()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clstObj, projObj, err := s.getObjectsForDeepLinks(ctx, app, proj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2035,7 +2044,7 @@ func (s *Server) resolveRevision(ctx context.Context, app *appv1.Application, sy
|
||||
func (s *Server) TerminateOperation(ctx context.Context, termOpReq *application.OperationTerminateRequest) (*application.OperationTerminateResponse, error) {
|
||||
appName := termOpReq.GetName()
|
||||
appNs := s.appNamespaceOrDefault(termOpReq.GetAppNamespace())
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "")
|
||||
a, _, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionSync, termOpReq.GetProject(), appNs, appName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2108,7 +2117,7 @@ func (s *Server) ListResourceActions(ctx context.Context, q *application.Applica
|
||||
|
||||
func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacRequest string, q *application.ApplicationResourceRequest) (obj *unstructured.Unstructured, res *appv1.ResourceNode, app *appv1.Application, config *rest.Config, err error) {
|
||||
if q.GetKind() == applicationType.ApplicationKind && q.GetGroup() == applicationType.Group && q.GetName() == q.GetResourceName() {
|
||||
app, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
app, _, err = s.getApplicationEnforceRBACInformer(ctx, rbacRequest, q.GetProject(), q.GetAppNamespace(), q.GetName())
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
@@ -2204,6 +2213,11 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA
|
||||
}
|
||||
}
|
||||
|
||||
proj, err := s.getAppProject(ctx, a, log.WithField("application", a.Name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, make sure all the returned resources are permitted, for each operation.
|
||||
// Also perform create with dry-runs for all create-operation resources.
|
||||
// This is performed separately to reduce the risk of only some of the resources being successfully created later.
|
||||
@@ -2211,7 +2225,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA
|
||||
// the dry-run for relevant apply/delete operation would have to be invoked as well.
|
||||
for _, impactedResource := range newObjects {
|
||||
newObj := impactedResource.UnstructuredObj
|
||||
err := s.verifyResourcePermitted(ctx, app, newObj)
|
||||
err := s.verifyResourcePermitted(ctx, app, proj, newObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2305,14 +2319,7 @@ func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObj
|
||||
return &application.ApplicationResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, obj *unstructured.Unstructured) error {
|
||||
proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
return fmt.Errorf("application references project %s which does not exist", app.Spec.Project)
|
||||
}
|
||||
return fmt.Errorf("failed to get project %s: %w", app.Spec.Project, err)
|
||||
}
|
||||
func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, proj *appv1.AppProject, obj *unstructured.Unstructured) error {
|
||||
permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) {
|
||||
clusters, err := s.db.GetProjectClusters(context.TODO(), project)
|
||||
if err != nil {
|
||||
@@ -2372,16 +2379,11 @@ func splitStatusPatch(patch []byte) ([]byte, []byte, error) {
|
||||
}
|
||||
|
||||
func (s *Server) GetApplicationSyncWindows(ctx context.Context, q *application.ApplicationSyncWindowsQuery) (*application.ApplicationSyncWindowsResponse, error) {
|
||||
a, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
a, proj, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetName(), "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting app project: %w", err)
|
||||
}
|
||||
|
||||
windows := proj.Spec.SyncWindows.Matches(a)
|
||||
sync := windows.CanSync(true)
|
||||
|
||||
|
||||
@@ -1439,6 +1439,27 @@ func TestCreateAppWithDestName(t *testing.T) {
|
||||
assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.com")
|
||||
}
|
||||
|
||||
// TestCreateAppWithOperation tests that an application created with an operation is created with the operation removed.
|
||||
// Avoids regressions of https://github.com/argoproj/argo-cd/security/advisories/GHSA-g623-jcgg-mhmm
|
||||
func TestCreateAppWithOperation(t *testing.T) {
|
||||
appServer := newTestAppServer(t)
|
||||
testApp := newTestAppWithDestName()
|
||||
testApp.Operation = &appsv1.Operation{
|
||||
Sync: &appsv1.SyncOperation{
|
||||
Manifests: []string{
|
||||
"test",
|
||||
},
|
||||
},
|
||||
}
|
||||
createReq := application.ApplicationCreateRequest{
|
||||
Application: testApp,
|
||||
}
|
||||
app, err := appServer.Create(context.Background(), &createReq)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, app)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
func TestUpdateApp(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
appServer := newTestAppServer(t, testApp)
|
||||
@@ -1797,7 +1818,7 @@ func TestServer_GetApplicationSyncWindowsState(t *testing.T) {
|
||||
appServer := newTestAppServer(t, testApp)
|
||||
|
||||
active, err := appServer.GetApplicationSyncWindows(context.Background(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name})
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
assert.Contains(t, err.Error(), "not exist")
|
||||
assert.Nil(t, active)
|
||||
})
|
||||
}
|
||||
@@ -2407,7 +2428,16 @@ func TestAppNamespaceRestrictions(t *testing.T) {
|
||||
t.Run("Get application in other namespace when allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
appServer := newTestAppServer(t, testApp)
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-1"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{
|
||||
Name: pointer.String("test-app"),
|
||||
@@ -2418,6 +2448,28 @@ func TestAppNamespaceRestrictions(t *testing.T) {
|
||||
require.Equal(t, "argocd-1", app.Namespace)
|
||||
require.Equal(t, "test-app", app.Name)
|
||||
})
|
||||
t.Run("Get application in other namespace when project is not allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-2"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
app, err := appServer.Get(context.TODO(), &application.ApplicationQuery{
|
||||
Name: pointer.String("test-app"),
|
||||
AppNamespace: pointer.String("argocd-1"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, app)
|
||||
require.ErrorContains(t, err, "app is not allowed in project")
|
||||
})
|
||||
t.Run("Create application in other namespace when allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
@@ -2460,7 +2512,7 @@ func TestAppNamespaceRestrictions(t *testing.T) {
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, app)
|
||||
require.ErrorContains(t, err, "not allowed to use project")
|
||||
require.ErrorContains(t, err, "app is not allowed in project")
|
||||
})
|
||||
|
||||
t.Run("Create application in other namespace when not allowed by configuration", func(t *testing.T) {
|
||||
@@ -2484,5 +2536,84 @@ func TestAppNamespaceRestrictions(t *testing.T) {
|
||||
require.Nil(t, app)
|
||||
require.ErrorContains(t, err, "namespace 'argocd-1' is not permitted")
|
||||
})
|
||||
|
||||
t.Run("Get application sync window in other namespace when project is allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-1"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(active.ActiveWindows))
|
||||
})
|
||||
t.Run("Get application sync window in other namespace when project is not allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-2"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
active, err := appServer.GetApplicationSyncWindows(context.TODO(), &application.ApplicationSyncWindowsQuery{Name: &testApp.Name, AppNamespace: &testApp.Namespace})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, active)
|
||||
require.ErrorContains(t, err, "app is not allowed in project")
|
||||
})
|
||||
t.Run("Get list of links in other namespace when project is not allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-2"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{
|
||||
Name: pointer.String("test-app"),
|
||||
Namespace: pointer.String("argocd-1"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, links)
|
||||
require.ErrorContains(t, err, "app is not allowed in project")
|
||||
})
|
||||
t.Run("Get list of links in other namespace when project is allowed", func(t *testing.T) {
|
||||
testApp := newTestApp()
|
||||
testApp.Namespace = "argocd-1"
|
||||
testApp.Spec.Project = "other-ns"
|
||||
otherNsProj := &appsv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "other-ns", Namespace: "default"},
|
||||
Spec: appsv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
SourceNamespaces: []string{"argocd-1"},
|
||||
},
|
||||
}
|
||||
appServer := newTestAppServer(t, testApp, otherNsProj)
|
||||
appServer.enabledNamespaces = []string{"argocd-1"}
|
||||
links, err := appServer.ListLinks(context.TODO(), &application.ListAppLinksRequest{
|
||||
Name: pointer.String("test-app"),
|
||||
Namespace: pointer.String("argocd-1"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(links.Items))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,12 +38,12 @@ type terminalHandler struct {
|
||||
allowedShells []string
|
||||
namespace string
|
||||
enabledNamespaces []string
|
||||
sessionManager util_session.SessionManager
|
||||
sessionManager *util_session.SessionManager
|
||||
}
|
||||
|
||||
// NewHandler returns a new terminal handler.
|
||||
func NewHandler(appLister applisters.ApplicationLister, namespace string, enabledNamespaces []string, db db.ArgoDB, enf *rbac.Enforcer, cache *servercache.Cache,
|
||||
appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager util_session.SessionManager) *terminalHandler {
|
||||
appResourceTree AppResourceTreeFn, allowedShells []string, sessionManager *util_session.SessionManager) *terminalHandler {
|
||||
return &terminalHandler{
|
||||
appLister: appLister,
|
||||
db: db,
|
||||
|
||||
@@ -37,7 +37,7 @@ type terminalSession struct {
|
||||
tty bool
|
||||
readLock sync.Mutex
|
||||
writeLock sync.Mutex
|
||||
sessionManager util_session.SessionManager
|
||||
sessionManager *util_session.SessionManager
|
||||
token *string
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func getToken(r *http.Request) (string, error) {
|
||||
}
|
||||
|
||||
// newTerminalSession create terminalSession
|
||||
func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager util_session.SessionManager) (*terminalSession, error) {
|
||||
func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
|
||||
token, err := getToken(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -992,10 +992,12 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
|
||||
}
|
||||
if len(a.ContentTypes) > 0 {
|
||||
handler = enforceContentTypes(handler, a.ContentTypes)
|
||||
} else {
|
||||
log.WithField(common.SecurityField, common.SecurityHigh).Warnf("Content-Type enforcement is disabled, which may make your API vulnerable to CSRF attacks")
|
||||
}
|
||||
mux.Handle("/api/", handler)
|
||||
|
||||
terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr).
|
||||
terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, a.sessionMgr).
|
||||
WithFeatureFlagMiddleware(a.settingsMgr.GetSettings)
|
||||
th := util_session.WithAuthMiddleware(a.DisableAuth, a.sessionMgr, terminal)
|
||||
mux.Handle("/terminal", th)
|
||||
|
||||
@@ -1425,3 +1425,46 @@ func TestReplaceBaseHRef(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_enforceContentTypes(t *testing.T) {
|
||||
getBaseHandler := func(t *testing.T, allow bool) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
assert.True(t, allow, "http handler was hit when it should have been blocked by content type enforcement")
|
||||
writer.WriteHeader(200)
|
||||
})
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
|
||||
t.Run("GET - not providing a content type, should still succeed", func(t *testing.T) {
|
||||
handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, 200, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("POST", func(t *testing.T) {
|
||||
handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc)
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp := w.Result()
|
||||
assert.Equal(t, 415, resp.StatusCode, "didn't provide a content type, should have gotten an error")
|
||||
|
||||
req = httptest.NewRequest("POST", "/", nil)
|
||||
req.Header = map[string][]string{"Content-Type": {"application/json"}}
|
||||
w = httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp = w.Result()
|
||||
assert.Equal(t, 200, resp.StatusCode, "should have passed, since an allowed content type was provided")
|
||||
|
||||
req = httptest.NewRequest("POST", "/", nil)
|
||||
req.Header = map[string][]string{"Content-Type": {"not-allowed"}}
|
||||
w = httptest.NewRecorder()
|
||||
handler(w, req)
|
||||
resp = w.Result()
|
||||
assert.Equal(t, 415, resp.StatusCode, "should not have passed, since a disallowed content type was provided")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -748,7 +748,7 @@ func TestNamespacedResourceDiffing(t *testing.T) {
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
And(func(app *Application) {
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata/guestbook")
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", "testdata/guestbook")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, diffOutput, fmt.Sprintf("===== apps/Deployment %s/guestbook-ui ======", DeploymentNamespace()))
|
||||
}).
|
||||
@@ -761,7 +761,7 @@ func TestNamespacedResourceDiffing(t *testing.T) {
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(app *Application) {
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata/guestbook")
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", "testdata/guestbook")
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, diffOutput)
|
||||
}).
|
||||
@@ -897,7 +897,7 @@ func testNSEdgeCasesApplicationResources(t *testing.T, appPath string, statusCod
|
||||
expect.
|
||||
Expect(HealthIs(statusCode)).
|
||||
And(func(app *Application) {
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", path.Join("testdata", appPath))
|
||||
diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", path.Join("testdata", appPath))
|
||||
assert.Empty(t, diffOutput)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
@@ -998,7 +998,7 @@ func TestNamespacedLocalManifestSync(t *testing.T) {
|
||||
Given().
|
||||
LocalPath(guestbookPathLocal).
|
||||
When().
|
||||
Sync().
|
||||
Sync("--local-repo-root", ".").
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(app *Application) {
|
||||
@@ -1066,7 +1066,7 @@ func TestNamespacedLocalSyncDryRunWithASEnabled(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
appBefore := app.DeepCopy()
|
||||
_, err = RunCli("app", "sync", app.QualifiedName(), "--dry-run", "--local", guestbookPathLocal)
|
||||
_, err = RunCli("app", "sync", app.QualifiedName(), "--dry-run", "--local-repo-root", ".", "--local", guestbookPathLocal)
|
||||
assert.NoError(t, err)
|
||||
|
||||
appAfter := app.DeepCopy()
|
||||
|
||||
@@ -1324,7 +1324,7 @@ func TestLocalManifestSync(t *testing.T) {
|
||||
Given().
|
||||
LocalPath(guestbookPathLocal).
|
||||
When().
|
||||
Sync().
|
||||
Sync("--local-repo-root", ".").
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(app *Application) {
|
||||
@@ -1385,7 +1385,7 @@ func TestLocalSyncDryRunWithAutosyncEnabled(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
appBefore := app.DeepCopy()
|
||||
_, err = RunCli("app", "sync", app.Name, "--dry-run", "--local", guestbookPathLocal)
|
||||
_, err = RunCli("app", "sync", app.Name, "--dry-run", "--local-repo-root", ".", "--local", guestbookPathLocal)
|
||||
assert.NoError(t, err)
|
||||
|
||||
appAfter := app.DeepCopy()
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
RevisionHelpIcon
|
||||
} from '../../../shared/components';
|
||||
import {BadgePanel, Spinner} from '../../../shared/components';
|
||||
import {Consumer, ContextApis} from '../../../shared/context';
|
||||
import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context';
|
||||
import * as models from '../../../shared/models';
|
||||
import {services} from '../../../shared/services';
|
||||
|
||||
@@ -30,6 +30,7 @@ import {EditAnnotations} from './edit-annotations';
|
||||
|
||||
import './application-summary.scss';
|
||||
import {DeepLinks} from '../../../shared/components/deep-links';
|
||||
import {ExternalLinks} from '../application-urls';
|
||||
|
||||
function swap(array: any[], a: number, b: number) {
|
||||
array = array.slice();
|
||||
@@ -47,6 +48,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
const source = getAppDefaultSource(app);
|
||||
const isHelm = source.hasOwnProperty('chart');
|
||||
const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL';
|
||||
const useAuthSettingsCtx = React.useContext(AuthSettingsCtx);
|
||||
const [destFormat, setDestFormat] = React.useState(initialState);
|
||||
const [changeSync, setChangeSync] = React.useState(false);
|
||||
|
||||
@@ -325,20 +327,19 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const urls = app.status.summary.externalURLs || [];
|
||||
const urls = ExternalLinks(app.status.summary.externalURLs);
|
||||
if (urls.length > 0) {
|
||||
attributes.push({
|
||||
title: 'URLs',
|
||||
view: (
|
||||
<React.Fragment>
|
||||
{urls
|
||||
.map(item => item.split('|'))
|
||||
.map((parts, i) => (
|
||||
<a key={i} href={parts.length > 1 ? parts[1] : parts[0]} target='__blank'>
|
||||
{parts[0]}
|
||||
{urls.map((url, i) => {
|
||||
return (
|
||||
<a key={i} href={url.ref} target='__blank'>
|
||||
{url.title}
|
||||
</a>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
)
|
||||
});
|
||||
@@ -589,7 +590,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
|
||||
</div>
|
||||
)}
|
||||
</Consumer>
|
||||
<BadgePanel app={props.app.metadata.name} />
|
||||
<BadgePanel app={props.app.metadata.name} appNamespace={props.app.metadata.namespace} nsEnabled={useAuthSettingsCtx?.appsInAnyNamespaceEnabled} />
|
||||
<EditablePanel
|
||||
save={updateApp}
|
||||
values={app}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ExternalLink, InvalidExternalLinkError} from './application-urls';
|
||||
import { ExternalLink, ExternalLinks, InvalidExternalLinkError } from './application-urls';
|
||||
|
||||
test('rejects malicious URLs', () => {
|
||||
expect(() => {
|
||||
@@ -7,6 +7,16 @@ test('rejects malicious URLs', () => {
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('title|data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:title|data:text/html;<h1>hi</h1>');
|
||||
}).toThrowError(InvalidExternalLinkError);
|
||||
|
||||
expect(() => {
|
||||
const _ = new ExternalLink('data:title|https://localhost:8080/applications');
|
||||
}).not.toThrowError(InvalidExternalLinkError);
|
||||
});
|
||||
|
||||
test('allows absolute URLs', () => {
|
||||
@@ -18,3 +28,59 @@ test('allows relative URLs', () => {
|
||||
window.location = new URL('https://localhost:8080/applications');
|
||||
expect(new ExternalLink('/applications').ref).toEqual('/applications');
|
||||
});
|
||||
|
||||
|
||||
test('URLs format', () => {
|
||||
expect(new ExternalLink('https://localhost:8080/applications')).toEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'https://localhost:8080/applications',
|
||||
})
|
||||
expect(new ExternalLink('title|https://localhost:8080/applications')).toEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'title',
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
test('malicious URLs from list to be removed', () => {
|
||||
const urls: string[] = [
|
||||
'javascript:alert("hi")',
|
||||
'https://localhost:8080/applications',
|
||||
]
|
||||
const links = ExternalLinks(urls);
|
||||
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links).toContainEqual({
|
||||
ref: 'https://localhost:8080/applications',
|
||||
title: 'https://localhost:8080/applications',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('list to be sorted', () => {
|
||||
const urls: string[] = [
|
||||
'https://a',
|
||||
'https://b',
|
||||
'a|https://c',
|
||||
'z|https://c',
|
||||
'x|https://d',
|
||||
'x|https://c',
|
||||
]
|
||||
const links = ExternalLinks(urls);
|
||||
|
||||
// 'a|https://c',
|
||||
// 'x|https://c',
|
||||
// 'x|https://d',
|
||||
// 'z|https://c',
|
||||
// 'https://a',
|
||||
// 'https://b',
|
||||
expect(links).toHaveLength(6);
|
||||
expect(links[0].title).toEqual('a')
|
||||
expect(links[1].title).toEqual('x')
|
||||
expect(links[1].ref).toEqual('https://c')
|
||||
expect(links[2].title).toEqual('x')
|
||||
expect(links[2].ref).toEqual('https://d')
|
||||
expect(links[3].title).toEqual('z')
|
||||
expect(links[4].title).toEqual('https://a')
|
||||
expect(links[5].title).toEqual('https://b')
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export class ExternalLink {
|
||||
}
|
||||
}
|
||||
|
||||
export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
export const ExternalLinks = (urls?: string[]) => {
|
||||
const externalLinks: ExternalLink[] = [];
|
||||
for (const url of urls || []) {
|
||||
try {
|
||||
@@ -42,16 +42,26 @@ export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
|
||||
// sorted alphabetically & links with titles first
|
||||
externalLinks.sort((a, b) => {
|
||||
if (a.title !== '' && b.title !== '') {
|
||||
const hasTitle = (x: ExternalLink): boolean => {
|
||||
return x.title !== x.ref && x.title !== '';
|
||||
};
|
||||
|
||||
if (hasTitle(a) && hasTitle(b) && a.title !== b.title) {
|
||||
return a.title > b.title ? 1 : -1;
|
||||
} else if (a.title === '') {
|
||||
} else if (hasTitle(b) && !hasTitle(a)) {
|
||||
return 1;
|
||||
} else if (b.title === '') {
|
||||
} else if (hasTitle(a) && !hasTitle(b)) {
|
||||
return -1;
|
||||
}
|
||||
return a.ref > b.ref ? 1 : -1;
|
||||
});
|
||||
|
||||
return externalLinks;
|
||||
};
|
||||
|
||||
export const ApplicationURLs = ({urls}: {urls: string[]}) => {
|
||||
const externalLinks: ExternalLink[] = ExternalLinks(urls);
|
||||
|
||||
return (
|
||||
((externalLinks || []).length > 0 && (
|
||||
<div className='applications-list__external-links-icon-container'>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {Context} from '../../context';
|
||||
|
||||
require('./badge-panel.scss');
|
||||
|
||||
export const BadgePanel = ({app, project}: {app?: string; project?: string}) => {
|
||||
export const BadgePanel = ({app, project, appNamespace, nsEnabled}: {app?: string; project?: string; appNamespace?: string; nsEnabled?: boolean}) => {
|
||||
const [badgeType, setBadgeType] = React.useState('URL');
|
||||
const context = React.useContext(Context);
|
||||
if (!app && !project) {
|
||||
@@ -20,6 +20,9 @@ export const BadgePanel = ({app, project}: {app?: string; project?: string}) =>
|
||||
let alt = '';
|
||||
if (app) {
|
||||
badgeURL = `${root}api/badge?name=${app}&revision=true`;
|
||||
if (nsEnabled) {
|
||||
badgeURL += `&namespace=${appNamespace}`;
|
||||
}
|
||||
entityURL = `${root}applications/${app}`;
|
||||
alt = 'App Status';
|
||||
} else if (project) {
|
||||
|
||||
@@ -51,19 +51,19 @@ export default {
|
||||
},
|
||||
|
||||
post(url: string) {
|
||||
return initHandlers(agent.post(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.post(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
put(url: string) {
|
||||
return initHandlers(agent.put(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.put(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
patch(url: string) {
|
||||
return initHandlers(agent.patch(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.patch(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
delete(url: string) {
|
||||
return initHandlers(agent.del(`${apiRoot()}${url}`));
|
||||
return initHandlers(agent.del(`${apiRoot()}${url}`)).set('Content-Type', 'application/json');
|
||||
},
|
||||
|
||||
loadEventSource(url: string): Observable<string> {
|
||||
|
||||
26
ui/yarn.lock
26
ui/yarn.lock
@@ -4596,10 +4596,10 @@ fs-minipass@^2.0.0:
|
||||
dependencies:
|
||||
minipass "^3.0.0"
|
||||
|
||||
fs-monkey@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3"
|
||||
integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==
|
||||
fs-monkey@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788"
|
||||
integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -6264,12 +6264,12 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
memfs@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305"
|
||||
integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==
|
||||
memfs@^3.4.3:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6"
|
||||
integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ==
|
||||
dependencies:
|
||||
fs-monkey "1.0.3"
|
||||
fs-monkey "^1.0.4"
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
@@ -9652,12 +9652,12 @@ webpack-cli@^4.9.2:
|
||||
webpack-merge "^5.7.3"
|
||||
|
||||
webpack-dev-middleware@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz#aa079a8dedd7e58bfeab358a9af7dab304cee57f"
|
||||
integrity sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==
|
||||
version "5.3.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517"
|
||||
integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==
|
||||
dependencies:
|
||||
colorette "^2.0.10"
|
||||
memfs "^3.4.1"
|
||||
memfs "^3.4.3"
|
||||
mime-types "^2.1.31"
|
||||
range-parser "^1.2.1"
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
@@ -694,8 +694,7 @@ func GetAppProject(app *argoappv1.Application, projLister applicationsv1.AppProj
|
||||
return nil, err
|
||||
}
|
||||
if !proj.IsAppNamespacePermitted(app, ns) {
|
||||
return nil, fmt.Errorf("application '%s' in namespace '%s' is not allowed to use project '%s'",
|
||||
app.Name, app.Namespace, proj.Name)
|
||||
return nil, argoappv1.NewErrApplicationNotAllowedToUseProject(app.Name, app.Namespace, proj.Name)
|
||||
}
|
||||
return proj, nil
|
||||
}
|
||||
|
||||
41
util/cache/redis.go
vendored
41
util/cache/redis.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
ioutil "github.com/argoproj/argo-cd/v2/util/io"
|
||||
@@ -155,41 +156,27 @@ type MetricsRegistry interface {
|
||||
ObserveRedisRequestDuration(duration time.Duration)
|
||||
}
|
||||
|
||||
var metricStartTimeKey = struct{}{}
|
||||
|
||||
type redisHook struct {
|
||||
registry MetricsRegistry
|
||||
}
|
||||
|
||||
func (rh *redisHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
|
||||
return context.WithValue(ctx, metricStartTimeKey, time.Now()), nil
|
||||
func (rh *redisHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := next(ctx, network, addr)
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
|
||||
func (rh *redisHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
|
||||
cmdErr := cmd.Err()
|
||||
rh.registry.IncRedisRequest(cmdErr != nil && cmdErr != redis.Nil)
|
||||
func (rh *redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
startTime := time.Now()
|
||||
|
||||
startTime := ctx.Value(metricStartTimeKey).(time.Time)
|
||||
duration := time.Since(startTime)
|
||||
rh.registry.ObserveRedisRequestDuration(duration)
|
||||
err := next(ctx, cmd)
|
||||
rh.registry.IncRedisRequest(err != nil && err != redis.Nil)
|
||||
rh.registry.ObserveRedisRequestDuration(time.Since(startTime))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) BeforeProcessPipeline(ctx context.Context, _ []redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (redisHook) AfterProcessPipeline(_ context.Context, _ []redis.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (redisHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||
|
||||
40
util/cache/redis_hook.go
vendored
40
util/cache/redis_hook.go
vendored
@@ -2,14 +2,13 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const NoSuchHostErr = "no such host"
|
||||
|
||||
type argoRedisHooks struct {
|
||||
reconnectCallback func()
|
||||
}
|
||||
@@ -18,32 +17,23 @@ func NewArgoRedisHook(reconnectCallback func()) *argoRedisHooks {
|
||||
return &argoRedisHooks{reconnectCallback: reconnectCallback}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) AfterProcess(ctx context.Context, cmd redis.Cmder) error {
|
||||
if cmd.Err() != nil && strings.Contains(cmd.Err().Error(), NoSuchHostErr) {
|
||||
log.Warnf("Reconnect to redis because error: \"%v\"", cmd.Err())
|
||||
hook.reconnectCallback()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) {
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) DialHook(next redis.DialHook) redis.DialHook {
|
||||
return nil
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := next(ctx, network, addr)
|
||||
return conn, err
|
||||
}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
return nil
|
||||
return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
var dnsError *net.DNSError
|
||||
err := next(ctx, cmd)
|
||||
if err != nil && errors.As(err, &dnsError) {
|
||||
log.Warnf("Reconnect to redis because error: \"%v\"", err)
|
||||
hook.reconnectCallback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (hook *argoRedisHooks) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
|
||||
|
||||
33
util/cache/redis_hook_test.go
vendored
33
util/cache/redis_hook_test.go
vendored
@@ -1,38 +1,53 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func Test_ReconnectCallbackHookCalled(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
called := false
|
||||
hook := NewArgoRedisHook(func() {
|
||||
called = true
|
||||
})
|
||||
|
||||
cmd := &redis.StringCmd{}
|
||||
cmd.SetErr(errors.New("Failed to resync revoked tokens. retrying again in 1 minute: dial tcp: lookup argocd-redis on 10.179.0.10:53: no such host"))
|
||||
|
||||
_ = hook.AfterProcess(context.Background(), cmd)
|
||||
faultyDNSRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"})
|
||||
faultyDNSRedisClient.AddHook(hook)
|
||||
|
||||
faultyDNSClient := NewRedisCache(faultyDNSRedisClient, 60*time.Second, RedisCompressionNone)
|
||||
err = faultyDNSClient.Set(&Item{Key: "baz", Object: "foo"})
|
||||
assert.Equal(t, called, true)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_ReconnectCallbackHookNotCalled(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
called := false
|
||||
hook := NewArgoRedisHook(func() {
|
||||
called = true
|
||||
})
|
||||
cmd := &redis.StringCmd{}
|
||||
cmd.SetErr(errors.New("Something wrong"))
|
||||
|
||||
_ = hook.AfterProcess(context.Background(), cmd)
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
redisClient.AddHook(hook)
|
||||
client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone)
|
||||
|
||||
err = client.Set(&Item{Key: "foo", Object: "bar"})
|
||||
assert.Equal(t, called, false)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
92
util/cache/redis_test.go
vendored
92
util/cache/redis_test.go
vendored
@@ -2,14 +2,59 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
promcm "github.com/prometheus/client_model/go"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
redisRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_redis_request_total",
|
||||
},
|
||||
[]string{"initiator", "failed"},
|
||||
)
|
||||
redisRequestHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_redis_request_duration",
|
||||
Buckets: []float64{0.1, 0.25, .5, 1, 2},
|
||||
},
|
||||
[]string{"initiator"},
|
||||
)
|
||||
)
|
||||
|
||||
type MockMetricsServer struct {
|
||||
registry *prometheus.Registry
|
||||
redisRequestCounter *prometheus.CounterVec
|
||||
redisRequestHistogram *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
func NewMockMetricsServer() *MockMetricsServer {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(redisRequestCounter)
|
||||
registry.MustRegister(redisRequestHistogram)
|
||||
return &MockMetricsServer{
|
||||
registry: registry,
|
||||
redisRequestCounter: redisRequestCounter,
|
||||
redisRequestHistogram: redisRequestHistogram,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockMetricsServer) IncRedisRequest(failed bool) {
|
||||
m.redisRequestCounter.WithLabelValues("mock", strconv.FormatBool(failed)).Inc()
|
||||
}
|
||||
|
||||
func (m *MockMetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
|
||||
m.redisRequestHistogram.WithLabelValues("mock").Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
func TestRedisSetCache(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
@@ -70,3 +115,50 @@ func TestRedisSetCacheCompressed(t *testing.T) {
|
||||
|
||||
assert.Equal(t, testValue, result)
|
||||
}
|
||||
|
||||
func TestRedisMetrics(t *testing.T) {
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer mr.Close()
|
||||
|
||||
metric := &promcm.Metric{}
|
||||
ms := NewMockMetricsServer()
|
||||
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
||||
faultyRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"})
|
||||
CollectMetrics(redisClient, ms)
|
||||
CollectMetrics(faultyRedisClient, ms)
|
||||
|
||||
client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone)
|
||||
faultyClient := NewRedisCache(faultyRedisClient, 60*time.Second, RedisCompressionNone)
|
||||
var res string
|
||||
|
||||
//client successful request
|
||||
err = client.Set(&Item{Key: "foo", Object: "bar"})
|
||||
assert.NoError(t, err)
|
||||
err = client.Get("foo", &res)
|
||||
assert.NoError(t, err)
|
||||
|
||||
c, err := ms.redisRequestCounter.GetMetricWithLabelValues("mock", "false")
|
||||
assert.NoError(t, err)
|
||||
err = c.Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, metric.Counter.GetValue(), float64(2))
|
||||
|
||||
//faulty client failed request
|
||||
err = faultyClient.Get("foo", &res)
|
||||
assert.Error(t, err)
|
||||
c, err = ms.redisRequestCounter.GetMetricWithLabelValues("mock", "true")
|
||||
assert.NoError(t, err)
|
||||
err = c.Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, metric.Counter.GetValue(), float64(1))
|
||||
|
||||
//both clients histogram count
|
||||
o, err := ms.redisRequestHistogram.GetMetricWithLabelValues("mock")
|
||||
assert.NoError(t, err)
|
||||
err = o.(prometheus.Metric).Write(metric)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int(metric.Histogram.GetSampleCount()), 3)
|
||||
}
|
||||
|
||||
13
util/env/env.go
vendored
13
util/env/env.go
vendored
@@ -124,8 +124,17 @@ func ParseDurationFromEnv(env string, defaultValue, min, max time.Duration) time
|
||||
return dur
|
||||
}
|
||||
|
||||
func StringFromEnv(env string, defaultValue string) string {
|
||||
if str := os.Getenv(env); str != "" {
|
||||
type StringFromEnvOpts struct {
|
||||
// AllowEmpty allows the value to be empty as long as the environment variable is set.
|
||||
AllowEmpty bool
|
||||
}
|
||||
|
||||
func StringFromEnv(env string, defaultValue string, opts ...StringFromEnvOpts) string {
|
||||
opt := StringFromEnvOpts{}
|
||||
for _, o := range opts {
|
||||
opt.AllowEmpty = opt.AllowEmpty || o.AllowEmpty
|
||||
}
|
||||
if str, ok := os.LookupEnv(env); opt.AllowEmpty && ok || str != "" {
|
||||
return str
|
||||
}
|
||||
return defaultValue
|
||||
|
||||
19
util/env/env_test.go
vendored
19
util/env/env_test.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestParseNumFromEnv(t *testing.T) {
|
||||
@@ -167,19 +168,25 @@ func TestStringFromEnv(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
env string
|
||||
env *string
|
||||
expected string
|
||||
def string
|
||||
opts []StringFromEnvOpts
|
||||
}{
|
||||
{"Some string", "true", "true", def},
|
||||
{"Empty string with default", "", def, def},
|
||||
{"Empty string without default", "", "", ""},
|
||||
{"Some string", pointer.String("true"), "true", def, nil},
|
||||
{"Empty string with default", pointer.String(""), def, def, nil},
|
||||
{"Empty string without default", pointer.String(""), "", "", nil},
|
||||
{"No env variable with default allow empty", nil, "default", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
{"Some variable with default allow empty", pointer.String("true"), "true", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
{"Empty variable with default allow empty", pointer.String(""), "", "default", []StringFromEnvOpts{{AllowEmpty: true}}},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv(envKey, tt.env)
|
||||
b := StringFromEnv(envKey, tt.def)
|
||||
if tt.env != nil {
|
||||
t.Setenv(envKey, *tt.env)
|
||||
}
|
||||
b := StringFromEnv(envKey, tt.def, tt.opts...)
|
||||
assert.Equal(t, tt.expected, b)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
executil "github.com/argoproj/argo-cd/v2/util/exec"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
executil "github.com/argoproj/argo-cd/v2/util/exec"
|
||||
|
||||
"github.com/argoproj/pkg/sync"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -34,6 +35,8 @@ import (
|
||||
var (
|
||||
globalLock = sync.NewKeyLock()
|
||||
indexLock = sync.NewKeyLock()
|
||||
|
||||
OCINotEnabledErr = errors.New("could not perform the action when oci is not enabled")
|
||||
)
|
||||
|
||||
type Creds struct {
|
||||
@@ -53,7 +56,7 @@ type indexCache interface {
|
||||
type Client interface {
|
||||
CleanChartCache(chart string, version string) error
|
||||
ExtractChart(chart string, version string, passCredentials bool, manifestMaxExtractedSize int64, disableManifestMaxExtractedSize bool) (string, argoio.Closer, error)
|
||||
GetIndex(noCache bool) (*Index, error)
|
||||
GetIndex(noCache bool, maxIndexSize int64) (*Index, error)
|
||||
GetTags(chart string, noCache bool) (*TagsList, error)
|
||||
TestHelmOCI() (bool, error)
|
||||
}
|
||||
@@ -227,7 +230,7 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (c *nativeHelmChart) GetIndex(noCache bool) (*Index, error) {
|
||||
func (c *nativeHelmChart) GetIndex(noCache bool, maxIndexSize int64) (*Index, error) {
|
||||
indexLock.Lock(c.repoURL)
|
||||
defer indexLock.Unlock(c.repoURL)
|
||||
|
||||
@@ -241,7 +244,7 @@ func (c *nativeHelmChart) GetIndex(noCache bool) (*Index, error) {
|
||||
if len(data) == 0 {
|
||||
start := time.Now()
|
||||
var err error
|
||||
data, err = c.loadRepoIndex()
|
||||
data, err = c.loadRepoIndex(maxIndexSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -294,7 +297,7 @@ func (c *nativeHelmChart) TestHelmOCI() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *nativeHelmChart) loadRepoIndex() ([]byte, error) {
|
||||
func (c *nativeHelmChart) loadRepoIndex(maxIndexSize int64) ([]byte, error) {
|
||||
indexURL, err := getIndexURL(c.repoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -329,7 +332,7 @@ func (c *nativeHelmChart) loadRepoIndex() ([]byte, error) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to get index: " + resp.Status)
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
return io.ReadAll(io.LimitReader(resp.Body, maxIndexSize))
|
||||
}
|
||||
|
||||
func newTLSConfig(creds Creds) (*tls.Config, error) {
|
||||
@@ -401,6 +404,10 @@ func getIndexURL(rawURL string) (string, error) {
|
||||
}
|
||||
|
||||
func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) {
|
||||
if !c.enableOci {
|
||||
return nil, OCINotEnabledErr
|
||||
}
|
||||
|
||||
tagsURL := strings.Replace(fmt.Sprintf("%s/%s", c.repoURL, chart), "https://", "", 1)
|
||||
indexLock.Lock(tagsURL)
|
||||
defer indexLock.Unlock(tagsURL)
|
||||
@@ -428,10 +435,12 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error)
|
||||
TLSClientConfig: tlsConf,
|
||||
DisableKeepAlives: true,
|
||||
}}
|
||||
|
||||
repoHost, _, _ := strings.Cut(tagsURL, "/")
|
||||
repo.Client = &auth.Client{
|
||||
Client: client,
|
||||
Cache: nil,
|
||||
Credential: auth.StaticCredential(c.repoURL, auth.Credential{
|
||||
Credential: auth.StaticCredential(repoHost, auth.Credential{
|
||||
Username: c.creds.Username,
|
||||
Password: c.creds.Password,
|
||||
}),
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -36,12 +37,12 @@ func (f *fakeIndexCache) GetHelmIndex(_ string, indexData *[]byte) error {
|
||||
func TestIndex(t *testing.T) {
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
client := NewClient("", Creds{}, false, "")
|
||||
_, err := client.GetIndex(false)
|
||||
_, err := client.GetIndex(false, 10000)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Stable", func(t *testing.T) {
|
||||
client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, "")
|
||||
index, err := client.GetIndex(false)
|
||||
index, err := client.GetIndex(false, 10000)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, index)
|
||||
})
|
||||
@@ -50,7 +51,7 @@ func TestIndex(t *testing.T) {
|
||||
Username: "my-password",
|
||||
Password: "my-username",
|
||||
}, false, "")
|
||||
index, err := client.GetIndex(false)
|
||||
index, err := client.GetIndex(false, 10000)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, index)
|
||||
})
|
||||
@@ -62,12 +63,18 @@ func TestIndex(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, "", WithIndexCache(&fakeIndexCache{data: data.Bytes()}))
|
||||
index, err := client.GetIndex(false)
|
||||
index, err := client.GetIndex(false, 10000)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, fakeIndex, *index)
|
||||
})
|
||||
|
||||
t.Run("Limited", func(t *testing.T) {
|
||||
client := NewClient("https://argoproj.github.io/argo-helm", Creds{}, false, "")
|
||||
_, err := client.GetIndex(false, 100)
|
||||
|
||||
assert.ErrorContains(t, err, "unexpected end of stream")
|
||||
})
|
||||
}
|
||||
|
||||
func Test_nativeHelmChart_ExtractChart(t *testing.T) {
|
||||
@@ -159,41 +166,129 @@ func TestGetIndexURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetTagsFromUrl(t *testing.T) {
|
||||
t.Run("should return tags correctly while following the link header", func(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("called %s", r.URL.Path)
|
||||
responseTags := TagsList{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if !strings.Contains(r.URL.String(), "token") {
|
||||
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
|
||||
responseTags.Tags = []string{"first"}
|
||||
} else {
|
||||
responseTags.Tags = []string{
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(responseTags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
|
||||
client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"first",
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("should return an error not when oci is not enabled", func(t *testing.T) {
|
||||
client := NewClient("example.com", Creds{}, false, "")
|
||||
|
||||
_, err := client.GetTags("my-chart", true)
|
||||
assert.ErrorIs(t, OCINotEnabledErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Logf("called %s", r.URL.Path)
|
||||
responseTags := TagsList{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if !strings.Contains(r.URL.String(), "token") {
|
||||
w.Header().Set("Link", fmt.Sprintf("<https://%s%s?token=next-token>; rel=next", r.Host, r.URL.Path))
|
||||
responseTags.Tags = []string{"first"}
|
||||
} else {
|
||||
responseTags.Tags = []string{
|
||||
"second",
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="helm repo to get tags"`)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("authorization received %s", authorization)
|
||||
|
||||
responseTags := TagsList{
|
||||
Tags: []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0_build",
|
||||
"2.8.0-prerelease_build",
|
||||
"2.8.0-prerelease.1_build.1234",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err := json.NewEncoder(w).Encode(responseTags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
serverURL, err := url.Parse(server.URL)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"first",
|
||||
"second",
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
repoURL string
|
||||
}{
|
||||
{
|
||||
name: "should login correctly when the repo path is in the server root with http scheme",
|
||||
repoURL: server.URL,
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is not in the server root with http scheme",
|
||||
repoURL: fmt.Sprintf("%s/my-repo", server.URL),
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is in the server root without http scheme",
|
||||
repoURL: serverURL.Host,
|
||||
},
|
||||
{
|
||||
name: "should login correctly when the repo path is not in the server root without http scheme",
|
||||
repoURL: fmt.Sprintf("%s/my-repo", serverURL.Host),
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
client := NewClient(testCase.repoURL, Creds{
|
||||
InsecureSkipVerify: true,
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
}, true, "")
|
||||
|
||||
tags, err := client.GetTags("mychart", true)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, tags.Tags, []string{
|
||||
"2.8.0",
|
||||
"2.8.0-prerelease",
|
||||
"2.8.0+build",
|
||||
"2.8.0-prerelease+build",
|
||||
"2.8.0-prerelease.1+build.1234",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,28 @@ func (c *Cmd) RegistryLogin(repo string, creds Creds) (string, error) {
|
||||
args = append(args, "--password", creds.Password)
|
||||
}
|
||||
|
||||
if creds.CAPath != "" {
|
||||
args = append(args, "--ca-file", creds.CAPath)
|
||||
}
|
||||
|
||||
if len(creds.CertData) > 0 {
|
||||
filePath, closer, err := writeToTmp(creds.CertData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer argoio.Close(closer)
|
||||
args = append(args, "--cert-file", filePath)
|
||||
}
|
||||
|
||||
if len(creds.KeyData) > 0 {
|
||||
filePath, closer, err := writeToTmp(creds.KeyData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer argoio.Close(closer)
|
||||
args = append(args, "--key-file", filePath)
|
||||
}
|
||||
|
||||
if creds.InsecureSkipVerify {
|
||||
args = append(args, "--insecure")
|
||||
}
|
||||
@@ -238,6 +260,25 @@ func (c *Cmd) PullOCI(repo string, chart string, version string, destination str
|
||||
if creds.CAPath != "" {
|
||||
args = append(args, "--ca-file", creds.CAPath)
|
||||
}
|
||||
|
||||
if len(creds.CertData) > 0 {
|
||||
filePath, closer, err := writeToTmp(creds.CertData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer argoio.Close(closer)
|
||||
args = append(args, "--cert-file", filePath)
|
||||
}
|
||||
|
||||
if len(creds.KeyData) > 0 {
|
||||
filePath, closer, err := writeToTmp(creds.KeyData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer argoio.Close(closer)
|
||||
args = append(args, "--key-file", filePath)
|
||||
}
|
||||
|
||||
if creds.InsecureSkipVerify && c.insecureSkipVerifySupported {
|
||||
args = append(args, "--insecure-skip-tls-verify")
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (_m *Client) ExtractChart(chart string, version string, passCredentials boo
|
||||
}
|
||||
|
||||
// GetIndex provides a mock function with given fields: noCache
|
||||
func (_m *Client) GetIndex(noCache bool) (*helm.Index, error) {
|
||||
func (_m *Client) GetIndex(noCache bool, maxIndexSize int64) (*helm.Index, error) {
|
||||
ret := _m.Called(noCache)
|
||||
|
||||
var r0 *helm.Index
|
||||
|
||||
@@ -35,8 +35,9 @@ type Kustomize interface {
|
||||
}
|
||||
|
||||
// NewKustomizeApp create a new wrapper to run commands on the `kustomize` command-line tool.
|
||||
func NewKustomizeApp(path string, creds git.Creds, fromRepo string, binaryPath string) Kustomize {
|
||||
func NewKustomizeApp(repoRoot string, path string, creds git.Creds, fromRepo string, binaryPath string) Kustomize {
|
||||
return &kustomize{
|
||||
repoRoot: repoRoot,
|
||||
path: path,
|
||||
creds: creds,
|
||||
repo: fromRepo,
|
||||
@@ -45,6 +46,8 @@ func NewKustomizeApp(path string, creds git.Creds, fromRepo string, binaryPath s
|
||||
}
|
||||
|
||||
type kustomize struct {
|
||||
// path to the Git repository root
|
||||
repoRoot string
|
||||
// path inside the checked out tree
|
||||
path string
|
||||
// creds structure
|
||||
@@ -281,6 +284,7 @@ func (k *kustomize) Build(opts *v1alpha1.ApplicationSourceKustomize, kustomizeOp
|
||||
}
|
||||
|
||||
cmd.Env = append(cmd.Env, environ...)
|
||||
cmd.Dir = k.repoRoot
|
||||
out, err := executil.Run(cmd)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestKustomizeBuild(t *testing.T) {
|
||||
namePrefix := "namePrefix-"
|
||||
nameSuffix := "-nameSuffix"
|
||||
namespace := "custom-namespace"
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "")
|
||||
env := &v1alpha1.Env{
|
||||
&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "argo-cd-tests"},
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func TestKustomizeBuild(t *testing.T) {
|
||||
func TestFailKustomizeBuild(t *testing.T) {
|
||||
appPath, err := testDataDir(t, kustomization1)
|
||||
assert.Nil(t, err)
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "")
|
||||
kustomizeSource := v1alpha1.ApplicationSourceKustomize{
|
||||
Replicas: []v1alpha1.KustomizeReplica{
|
||||
{
|
||||
@@ -221,7 +221,7 @@ func TestKustomizeBuildForceCommonLabels(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
appPath, err := testDataDir(t, tc.TestData)
|
||||
assert.Nil(t, err)
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "")
|
||||
objs, _, err := kustomize.Build(&tc.KustomizeSource, nil, tc.Env)
|
||||
switch tc.ExpectErr {
|
||||
case true:
|
||||
@@ -313,7 +313,7 @@ func TestKustomizeBuildForceCommonAnnotations(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
appPath, err := testDataDir(t, tc.TestData)
|
||||
assert.Nil(t, err)
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "")
|
||||
objs, _, err := kustomize.Build(&tc.KustomizeSource, nil, tc.Env)
|
||||
switch tc.ExpectErr {
|
||||
case true:
|
||||
@@ -333,7 +333,7 @@ func TestKustomizeCustomVersion(t *testing.T) {
|
||||
kustomizePath, err := testDataDir(t, kustomization4)
|
||||
assert.Nil(t, err)
|
||||
envOutputFile := kustomizePath + "/env_output"
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", kustomizePath+"/kustomize.special")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", kustomizePath+"/kustomize.special")
|
||||
kustomizeSource := v1alpha1.ApplicationSourceKustomize{
|
||||
Version: "special",
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func TestKustomizeCustomVersion(t *testing.T) {
|
||||
func TestKustomizeBuildPatches(t *testing.T) {
|
||||
appPath, err := testDataDir(t, kustomization5)
|
||||
assert.Nil(t, err)
|
||||
kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "")
|
||||
kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "")
|
||||
|
||||
kustomizeSource := v1alpha1.ApplicationSourceKustomize{
|
||||
Patches: []v1alpha1.KustomizePatch{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -41,6 +42,7 @@ type SessionManager struct {
|
||||
storage UserStateStorage
|
||||
sleep func(d time.Duration)
|
||||
verificationDelayNoiseEnabled bool
|
||||
failedLock sync.RWMutex
|
||||
}
|
||||
|
||||
// LoginAttempts is a timestamped counter for failed login attempts
|
||||
@@ -69,7 +71,7 @@ const (
|
||||
// Maximum length of username, too keep the cache's memory signature low
|
||||
maxUsernameLength = 32
|
||||
// The default maximum session cache size
|
||||
defaultMaxCacheSize = 1000
|
||||
defaultMaxCacheSize = 10000
|
||||
// The default number of maximum login failures before delay kicks in
|
||||
defaultMaxLoginFailures = 5
|
||||
// The default time in seconds for the failure window
|
||||
@@ -284,7 +286,7 @@ func (mgr *SessionManager) Parse(tokenString string) (jwt.Claims, string, error)
|
||||
return token.Claims, newToken, nil
|
||||
}
|
||||
|
||||
// GetLoginFailures retrieves the login failure information from the cache
|
||||
// GetLoginFailures retrieves the login failure information from the cache. Any modifications to the LoginAttemps map must be done in a thread-safe manner.
|
||||
func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
|
||||
// Get failures from the cache
|
||||
var failures map[string]LoginAttempts
|
||||
@@ -299,25 +301,43 @@ func (mgr *SessionManager) GetLoginFailures() map[string]LoginAttempts {
|
||||
return failures
|
||||
}
|
||||
|
||||
func expireOldFailedAttempts(maxAge time.Duration, failures *map[string]LoginAttempts) int {
|
||||
func expireOldFailedAttempts(maxAge time.Duration, failures map[string]LoginAttempts) int {
|
||||
expiredCount := 0
|
||||
for key, attempt := range *failures {
|
||||
for key, attempt := range failures {
|
||||
if time.Since(attempt.LastFailed) > maxAge*time.Second {
|
||||
expiredCount += 1
|
||||
delete(*failures, key)
|
||||
delete(failures, key)
|
||||
}
|
||||
}
|
||||
return expiredCount
|
||||
}
|
||||
|
||||
// Protect admin user from login attempt reset caused by attempts to overflow cache in a brute force attack. Instead remove random non-admin to make room in cache.
|
||||
func pickRandomNonAdminLoginFailure(failures map[string]LoginAttempts, username string) *string {
|
||||
idx := rand.Intn(len(failures) - 1)
|
||||
i := 0
|
||||
for key := range failures {
|
||||
if i == idx {
|
||||
if key == common.ArgoCDAdminUsername || key == username {
|
||||
return pickRandomNonAdminLoginFailure(failures, username)
|
||||
}
|
||||
return &key
|
||||
}
|
||||
i++
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Updates the failure count for a given username. If failed is true, increases the counter. Otherwise, sets counter back to 0.
|
||||
func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
mgr.failedLock.Lock()
|
||||
defer mgr.failedLock.Unlock()
|
||||
|
||||
failures := mgr.GetLoginFailures()
|
||||
|
||||
// Expire old entries in the cache if we have a failure window defined.
|
||||
if window := getLoginFailureWindow(); window > 0 {
|
||||
count := expireOldFailedAttempts(window, &failures)
|
||||
count := expireOldFailedAttempts(window, failures)
|
||||
if count > 0 {
|
||||
log.Infof("Expired %d entries from session cache due to max age reached", count)
|
||||
}
|
||||
@@ -327,23 +347,13 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
// prevent overbloating the cache with fake entries, as this could lead to
|
||||
// memory exhaustion and ultimately in a DoS. We remove a single entry to
|
||||
// replace it with the new one.
|
||||
//
|
||||
// Chances are that we remove the one that is under active attack, but this
|
||||
// chance is low (1:cache_size)
|
||||
if failed && len(failures) >= getMaximumCacheSize() {
|
||||
log.Warnf("Session cache size exceeds %d entries, removing random entry", getMaximumCacheSize())
|
||||
idx := rand.Intn(len(failures) - 1)
|
||||
var rmUser string
|
||||
i := 0
|
||||
for key := range failures {
|
||||
if i == idx {
|
||||
rmUser = key
|
||||
delete(failures, key)
|
||||
break
|
||||
}
|
||||
i++
|
||||
rmUser := pickRandomNonAdminLoginFailure(failures, username)
|
||||
if rmUser != nil {
|
||||
delete(failures, *rmUser)
|
||||
log.Infof("Deleted entry for user %s from cache", *rmUser)
|
||||
}
|
||||
log.Infof("Deleted entry for user %s from cache", rmUser)
|
||||
}
|
||||
|
||||
attempt, ok := failures[username]
|
||||
@@ -374,6 +384,8 @@ func (mgr *SessionManager) updateFailureCount(username string, failed bool) {
|
||||
|
||||
// Get the current login failure attempts for given username
|
||||
func (mgr *SessionManager) getFailureCount(username string) LoginAttempts {
|
||||
mgr.failedLock.RLock()
|
||||
defer mgr.failedLock.RUnlock()
|
||||
failures := mgr.GetLoginFailures()
|
||||
attempt, ok := failures[username]
|
||||
if !ok {
|
||||
|
||||
@@ -1173,3 +1173,42 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
||||
assert.ErrorIs(t, err, common.TokenVerificationErr)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
|
||||
t.Run("Not pick admin user from the queue", func(t *testing.T) {
|
||||
failures := map[string]LoginAttempts{
|
||||
"admin": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"test2": {
|
||||
FailCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
||||
for i := 0; i < 1000; i++ {
|
||||
user := pickRandomNonAdminLoginFailure(failures, "test")
|
||||
assert.Equal(t, "test2", *user)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
|
||||
failures := map[string]LoginAttempts{
|
||||
"test": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"admin": {
|
||||
FailCount: 1,
|
||||
},
|
||||
"test2": {
|
||||
FailCount: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
||||
for i := 0; i < 1000; i++ {
|
||||
user := pickRandomNonAdminLoginFailure(failures, "test")
|
||||
assert.Equal(t, "test2", *user)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// Cert is a certificate for tests. It was generated like this:
|
||||
|
||||
Reference in New Issue
Block a user