mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
feat(otel): add spans with configurable sampling (#28396)
Signed-off-by: Blake Pettersson <[email protected]>
This commit is contained in:
@@ -86,6 +86,7 @@ func NewCommand() *cobra.Command {
|
||||
otlpInsecure bool
|
||||
otlpHeaders map[string]string
|
||||
otlpAttrs []string
|
||||
otlpSampleRatio float64
|
||||
applicationNamespaces []string
|
||||
persistResourceHealth bool
|
||||
shardingAlgorithm string
|
||||
@@ -229,7 +230,7 @@ func NewCommand() *cobra.Command {
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
if otlpAddress != "" {
|
||||
closeTracer, err := trace.InitTracer(ctx, "argocd-controller", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs)
|
||||
closeTracer, err := trace.InitTracer(ctx, "argocd-controller", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs, otlpSampleRatio)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize tracing: %v", err)
|
||||
}
|
||||
@@ -289,6 +290,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode")
|
||||
command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)")
|
||||
command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)")
|
||||
cli.BoundedFloat64Var(command.Flags(), &otlpSampleRatio, "otlp-sample-ratio", env.ParseFloat64FromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO", 1.0, 0.0, 1.0), 0.0, 1.0, "Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision")
|
||||
command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that applications are allowed to be reconciled from")
|
||||
command.Flags().BoolVar(&persistResourceHealth, "persist-resource-health", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH", false), "Enables storing the managed resources health in the Application CRD")
|
||||
command.Flags().StringVar(&shardingAlgorithm, "sharding-method", env.StringFromEnv(common.EnvControllerShardingAlgorithm, common.DefaultShardingAlgorithm), "Enables choice of sharding method. Supported sharding methods are : [legacy, round-robin, consistent-hashing] ")
|
||||
|
||||
@@ -20,11 +20,12 @@ import (
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
configFilePath string
|
||||
otlpAddress string
|
||||
otlpInsecure bool
|
||||
otlpHeaders map[string]string
|
||||
otlpAttrs []string
|
||||
configFilePath string
|
||||
otlpAddress string
|
||||
otlpInsecure bool
|
||||
otlpHeaders map[string]string
|
||||
otlpAttrs []string
|
||||
otlpSampleRatio float64
|
||||
)
|
||||
command := cobra.Command{
|
||||
Use: common.CommandCMPServer,
|
||||
@@ -61,7 +62,7 @@ func NewCommand() *cobra.Command {
|
||||
if otlpAddress != "" {
|
||||
var closer func()
|
||||
var err error
|
||||
closer, err = traceutil.InitTracer(ctx, "argocd-cmp-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs)
|
||||
closer, err = traceutil.InitTracer(ctx, "argocd-cmp-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs, otlpSampleRatio)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize tracing: %v", err)
|
||||
}
|
||||
@@ -91,5 +92,6 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_CMP_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode")
|
||||
command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_CMP_SERVER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)")
|
||||
command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_CMP_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)")
|
||||
cli.BoundedFloat64Var(command.Flags(), &otlpSampleRatio, "otlp-sample-ratio", env.ParseFloat64FromEnv("ARGOCD_CMP_SERVER_OTLP_SAMPLE_RATIO", 1.0, 0.0, 1.0), 0.0, 1.0, "Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision")
|
||||
return &command
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ func NewCommand() *cobra.Command {
|
||||
otlpInsecure bool
|
||||
otlpHeaders map[string]string
|
||||
otlpAttrs []string
|
||||
otlpSampleRatio float64
|
||||
cacheSrc func() (*reposervercache.Cache, error)
|
||||
tlsConfigCustomizer tls.ConfigCustomizer
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
@@ -171,7 +172,7 @@ func NewCommand() *cobra.Command {
|
||||
if otlpAddress != "" {
|
||||
var closer func()
|
||||
var err error
|
||||
closer, err = traceutil.InitTracer(ctx, "argocd-repo-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs)
|
||||
closer, err = traceutil.InitTracer(ctx, "argocd-repo-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs, otlpSampleRatio)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize tracing: %v", err)
|
||||
}
|
||||
@@ -263,6 +264,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode")
|
||||
command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_REPO_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)")
|
||||
command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_REPO_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)")
|
||||
cli.BoundedFloat64Var(command.Flags(), &otlpSampleRatio, "otlp-sample-ratio", env.ParseFloat64FromEnv("ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO", 1.0, 0.0, 1.0), 0.0, 1.0, "Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision")
|
||||
command.Flags().StringVar(&maxCombinedDirectoryManifestsSize, "max-combined-directory-manifests-size", env.StringFromEnv("ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE", "10M"), "Max combined size of manifest files in a directory-type Application")
|
||||
command.Flags().StringArrayVar(&cmpTarExcludedGlobs, "plugin-tar-exclude", env.StringsFromEnv("ARGOCD_REPO_SERVER_PLUGIN_TAR_EXCLUSIONS", []string{}, ";"), "Globs to filter when sending tarballs to plugins.")
|
||||
command.Flags().BoolVar(&allowOutOfBoundsSymlinks, "allow-oob-symlinks", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_ALLOW_OUT_OF_BOUNDS_SYMLINKS", false), "Allow out-of-bounds symlinks in repositories (not recommended)")
|
||||
|
||||
@@ -65,6 +65,7 @@ func NewCommand() *cobra.Command {
|
||||
otlpInsecure bool
|
||||
otlpHeaders map[string]string
|
||||
otlpAttrs []string
|
||||
otlpSampleRatio float64
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
repoServerTimeoutSeconds int
|
||||
@@ -279,7 +280,7 @@ func NewCommand() *cobra.Command {
|
||||
lns, err := argocd.Listen()
|
||||
errors.CheckError(err)
|
||||
if otlpAddress != "" {
|
||||
closer, err = traceutil.InitTracer(serverCtx, "argocd-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs)
|
||||
closer, err = traceutil.InitTracer(serverCtx, "argocd-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs, otlpSampleRatio)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize tracing: %v", err)
|
||||
}
|
||||
@@ -325,6 +326,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode")
|
||||
command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_SERVER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)")
|
||||
command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)")
|
||||
cli.BoundedFloat64Var(command.Flags(), &otlpSampleRatio, "otlp-sample-ratio", env.ParseFloat64FromEnv("ARGOCD_SERVER_OTLP_SAMPLE_RATIO", 1.0, 0.0, 1.0), 0.0, 1.0, "Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.")
|
||||
command.Flags().StringVar(&frameOptions, "x-frame-options", env.StringFromEnv("ARGOCD_SERVER_X_FRAME_OPTIONS", "sameorigin"), "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
|
||||
command.Flags().StringVar(&contentSecurityPolicy, "content-security-policy", env.StringFromEnv("ARGOCD_SERVER_CONTENT_SECURITY_POLICY", "frame-ancestors 'self';"), "Set Content-Security-Policy header in HTTP responses to `value`. To disable, set to \"\".")
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/gitops-engine/v3/pkg/utils/kube"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
otel_codes "go.opentelemetry.io/otel/codes"
|
||||
"golang.org/x/sync/semaphore"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -72,6 +73,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/helm"
|
||||
logutils "github.com/argoproj/argo-cd/v3/util/log"
|
||||
settings_util "github.com/argoproj/argo-cd/v3/util/settings"
|
||||
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -1215,7 +1217,10 @@ func (ctrl *ApplicationController) getPermittedAppLiveObjects(destCluster *appv1
|
||||
return objsMap, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(ctx context.Context, app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) error {
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(ctx context.Context, app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) (retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "controller.finalizeApplicationDeletion")
|
||||
setAppTraceAttrs(span, app)
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
// Get refreshed application info, since informer app copy might be stale
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(ctx, app.Name, metav1.GetOptions{})
|
||||
@@ -1450,13 +1455,26 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
// processAppOperationQueueItem is a workqueue entry point with no inbound request
|
||||
// context, so context.Background() roots this operation's context tree.
|
||||
ctx := context.Background()
|
||||
ctx, span := tracer.Start(ctx, "controller.Operation")
|
||||
setAppTraceAttrs(span, app)
|
||||
var state *appv1.OperationState
|
||||
// Registered first so it runs last: after the panic-recovery and timing defers below have
|
||||
// finalized state. The operation reports failure via state.Phase (not a return value), so map
|
||||
// a terminal failed phase onto the span status; panics are handled by the recovery defer.
|
||||
defer func() {
|
||||
if state != nil && state.Phase.Failed() {
|
||||
span.SetStatus(otel_codes.Error, state.Message)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
// Recover from any unexpected panics and automatically set the status to be failed
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logCtx.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
span.SetStatus(otel_codes.Error, fmt.Sprintf("%v", r))
|
||||
state.Phase = synccommon.OperationError
|
||||
if rerr, ok := r.(error); ok {
|
||||
span.RecordError(rerr)
|
||||
state.Message = rerr.Error()
|
||||
} else {
|
||||
state.Message = fmt.Sprintf("%v", r)
|
||||
@@ -1760,6 +1778,9 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
// processAppRefreshQueueItem is a workqueue entry point with no inbound request
|
||||
// context, so context.Background() roots this reconciliation's context tree.
|
||||
ctx := context.Background()
|
||||
ctx, span := tracer.Start(ctx, "controller.Refresh")
|
||||
setAppTraceAttrs(span, origApp)
|
||||
defer span.End()
|
||||
|
||||
startTime := time.Now()
|
||||
ts := stats.NewTimingStats()
|
||||
@@ -1798,7 +1819,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
}
|
||||
}
|
||||
|
||||
patchDuration = ctrl.persistReconciliationStatus(origApp, &app.Status)
|
||||
patchDuration = ctrl.persistReconciliationStatus(ctx, origApp, &app.Status)
|
||||
return processNext
|
||||
}
|
||||
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fall back to full reconciliation")
|
||||
@@ -1813,7 +1834,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = health.HealthStatusUnknown
|
||||
app.Status.Health.Message = ""
|
||||
patchDuration = ctrl.persistReconciliationStatus(origApp, &app.Status)
|
||||
patchDuration = ctrl.persistReconciliationStatus(ctx, origApp, &app.Status)
|
||||
|
||||
if err := ctrl.cache.SetAppResourcesTree(app.InstanceName(ctrl.namespace), &appv1.ApplicationTree{}); err != nil {
|
||||
logCtx.WithError(err).Warn("failed to set app resource tree")
|
||||
@@ -1896,7 +1917,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
// decision: when the app is OutOfSync, always let autoSync compare the desired revision
|
||||
// against the last synced one (#27875).
|
||||
shouldCompareRevisions := compareResult.revisionsMayHaveChanges || compareResult.syncStatus.Status == appv1.SyncStatusCodeOutOfSync
|
||||
syncErrCond, opDuration := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources, shouldCompareRevisions)
|
||||
syncErrCond, opDuration := ctrl.autoSync(ctx, app, compareResult.syncStatus, compareResult.resources, shouldCompareRevisions)
|
||||
setOpDuration = opDuration
|
||||
if syncErrCond != nil {
|
||||
app.Status.SetConditions(
|
||||
@@ -1961,7 +1982,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
}
|
||||
}
|
||||
ts.AddCheckpoint("process_finalizers_ms")
|
||||
patchDuration = ctrl.persistReconciliationStatus(origApp, &app.Status)
|
||||
patchDuration = ctrl.persistReconciliationStatus(ctx, origApp, &app.Status)
|
||||
// This is a partly a duplicate of patch_ms, but more descriptive and allows to have measurement for the next step.
|
||||
ts.AddCheckpoint("persist_app_status_ms")
|
||||
return processNext
|
||||
@@ -2160,16 +2181,25 @@ func createMergePatch(orig, newV any) ([]byte, bool, error) {
|
||||
}
|
||||
|
||||
// persistReconciliationStatus persists updates to application status and consumes the refresh annotation.
|
||||
func (ctrl *ApplicationController) persistReconciliationStatus(orig *appv1.Application, newStatus *appv1.ApplicationStatus) time.Duration {
|
||||
func (ctrl *ApplicationController) persistReconciliationStatus(ctx context.Context, orig *appv1.Application, newStatus *appv1.ApplicationStatus) time.Duration {
|
||||
newAnnotations := make(map[string]string)
|
||||
maps.Copy(newAnnotations, orig.GetAnnotations())
|
||||
delete(newAnnotations, appv1.AnnotationKeyRefresh)
|
||||
return ctrl.persistAppStatus(orig, newStatus, newAnnotations)
|
||||
return ctrl.persistAppStatus(ctx, orig, newStatus, newAnnotations)
|
||||
}
|
||||
|
||||
// persistAppStatus persists updates to application status and optionally updates annotations.
|
||||
// If no changes were made, it is a no-op
|
||||
func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, newStatus *appv1.ApplicationStatus, newAnnotations map[string]string) (patchDuration time.Duration) {
|
||||
func (ctrl *ApplicationController) persistAppStatus(ctx context.Context, orig *appv1.Application, newStatus *appv1.ApplicationStatus, newAnnotations map[string]string) (patchDuration time.Duration) {
|
||||
// NB: leaf span only — the status patch below deliberately stays on context.Background() so a
|
||||
// canceled reconcile ctx never aborts a durable status write. The span just measures it.
|
||||
_, span := tracer.Start(ctx, "controller.persistAppStatus")
|
||||
setAppTraceAttrs(span, orig)
|
||||
// spanErr records a failed status write on the span so a trace doesn't look successful when
|
||||
// the patch below failed. These are Kubernetes API/merge-patch errors, not repo URLs, so
|
||||
// recording the message via EndSpan does not risk leaking credentials.
|
||||
var spanErr error
|
||||
defer func() { traceutil.EndSpan(span, spanErr) }()
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(orig))
|
||||
if orig.Status.Sync.Status != newStatus.Sync.Status {
|
||||
message := fmt.Sprintf("Updated sync status: %s -> %s", orig.Status.Sync.Status, newStatus.Sync.Status)
|
||||
@@ -2200,6 +2230,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: orig.GetAnnotations()}, Status: orig.Status},
|
||||
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: newAnnotations}, Status: *newStatus})
|
||||
if err != nil {
|
||||
spanErr = err
|
||||
logCtx.WithError(err).Error("Error constructing app status patch")
|
||||
return patchDuration
|
||||
}
|
||||
@@ -2214,6 +2245,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
}()
|
||||
_, err = ctrl.PatchAppWithWriteBack(context.Background(), orig.Name, orig.Namespace, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
spanErr = err
|
||||
if apierrors.IsRequestEntityTooLargeError(err) {
|
||||
logCtx.WithError(err).Warn("Application status exceeds the Kubernetes resource size limit; falling back to error condition only")
|
||||
fallbackStatus := orig.Status.DeepCopy()
|
||||
@@ -2261,7 +2293,10 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
}
|
||||
|
||||
// autoSync will initiate a sync operation for an application configured with automated sync
|
||||
func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus, shouldCompareRevisions bool) (*appv1.ApplicationCondition, time.Duration) {
|
||||
func (ctrl *ApplicationController) autoSync(ctx context.Context, app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus, shouldCompareRevisions bool) (*appv1.ApplicationCondition, time.Duration) {
|
||||
_, span := tracer.Start(ctx, "controller.autoSync")
|
||||
setAppTraceAttrs(span, app)
|
||||
defer span.End()
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
ts := stats.NewTimingStats()
|
||||
defer func() {
|
||||
|
||||
@@ -681,7 +681,7 @@ func TestAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -698,7 +698,7 @@ func TestAutoSyncEnabledSetToTrue(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -719,7 +719,7 @@ func TestAutoSyncMultiSourceWithoutSelfHeal(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revisions: []string{"z", "x", "v"},
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -734,7 +734,7 @@ func TestAutoSyncMultiSourceWithoutSelfHeal(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revisions: []string{"a", "b", "c"},
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook-1", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -759,7 +759,7 @@ func TestAutoSyncManifestGeneratePathsNewCommit(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: revB,
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, syncStatus.Status == v1alpha1.SyncStatusCodeOutOfSync)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, syncStatus.Status == v1alpha1.SyncStatusCodeOutOfSync)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -779,7 +779,7 @@ func TestAutoSyncManifestGeneratePathsNewCommit(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: revB,
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, syncStatus.Status == v1alpha1.SyncStatusCodeOutOfSync)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, syncStatus.Status == v1alpha1.SyncStatusCodeOutOfSync)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -795,7 +795,7 @@ func TestAutoSyncNotAllowEmpty(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.NotNil(t, cond)
|
||||
}
|
||||
|
||||
@@ -808,7 +808,7 @@ func TestAutoSyncAllowEmpty(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
}
|
||||
|
||||
@@ -822,7 +822,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -837,7 +837,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeSynced,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -853,7 +853,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -869,7 +869,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -886,7 +886,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -912,7 +912,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -936,7 +936,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -950,7 +950,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{
|
||||
{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync, RequiresPruning: true},
|
||||
}, true)
|
||||
assert.Nil(t, cond)
|
||||
@@ -988,7 +988,7 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -1032,7 +1032,7 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
ctrl := newFakeController(t.Context(), &fakeData{apps: []runtime.Object{app}}, nil)
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -1063,7 +1063,7 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Revisions: []string{"z", "x", "v"},
|
||||
}
|
||||
cond, _ := ctrl.autoSync(app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
cond, _ := ctrl.autoSync(t.Context(), app, &syncStatus, []v1alpha1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: v1alpha1.SyncStatusCodeOutOfSync}}, true)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(t.Context(), "my-app", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -3444,7 +3444,7 @@ func TestApplicationController_PersistAppStatus_FallbackOnSizeLimit(t *testing.T
|
||||
{Name: "bloat-1", Kind: "ConfigMap", Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}
|
||||
|
||||
ctrl.persistAppStatus(app, newStatus, app.GetAnnotations())
|
||||
ctrl.persistAppStatus(t.Context(), app, newStatus, app.GetAnnotations())
|
||||
|
||||
require.Equal(t, 2, patchCalls, "expected initial patch + fallback patch")
|
||||
|
||||
@@ -3487,7 +3487,7 @@ func TestApplicationController_PersistAppStatus_NonSizeLimitErrorNoFallback(t *t
|
||||
newStatus := app.Status.DeepCopy()
|
||||
newStatus.Sync.Status = v1alpha1.SyncStatusCodeOutOfSync
|
||||
|
||||
ctrl.persistAppStatus(app, newStatus, app.GetAnnotations())
|
||||
ctrl.persistAppStatus(t.Context(), app, newStatus, app.GetAnnotations())
|
||||
|
||||
assert.Equal(t, 1, patchCalls, "non-size-limit errors should NOT trigger fallback patch")
|
||||
}
|
||||
@@ -3523,7 +3523,7 @@ func TestApplicationController_PersistAppStatus_FallbackMessageContainsUserGuida
|
||||
{Name: "bloat-1", Kind: "ConfigMap", Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}
|
||||
|
||||
ctrl.persistAppStatus(app, newStatus, app.GetAnnotations())
|
||||
ctrl.persistAppStatus(t.Context(), app, newStatus, app.GetAnnotations())
|
||||
|
||||
require.Equal(t, 2, patchCalls, "expected initial patch + fallback patch")
|
||||
|
||||
@@ -3580,7 +3580,7 @@ func TestApplicationController_PersistAppStatus_FallbackPatchAlsoFails(t *testin
|
||||
// Must not panic or block when the fallback patch also fails — the error
|
||||
// should be logged and persistAppStatus should return normally.
|
||||
assert.NotPanics(t, func() {
|
||||
ctrl.persistAppStatus(app, newStatus, app.GetAnnotations())
|
||||
ctrl.persistAppStatus(t.Context(), app, newStatus, app.GetAnnotations())
|
||||
})
|
||||
|
||||
assert.Equal(t, 2, patchCalls, "fallback patch should be attempted exactly once after initial size-limit failure")
|
||||
@@ -4142,7 +4142,7 @@ func TestPersistAppStatus_AnnotationManagement(t *testing.T) {
|
||||
origApp := app.DeepCopy()
|
||||
newStatus := app.Status.DeepCopy()
|
||||
|
||||
ctrl.persistReconciliationStatus(origApp, newStatus)
|
||||
ctrl.persistReconciliationStatus(t.Context(), origApp, newStatus)
|
||||
|
||||
// Verify the patch was created correctly
|
||||
patchedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
|
||||
@@ -4183,7 +4183,7 @@ func TestPersistAppStatus_AnnotationManagement(t *testing.T) {
|
||||
maps.Copy(newAnnotations, origApp.Annotations)
|
||||
delete(newAnnotations, v1alpha1.AnnotationKeyHydrate)
|
||||
|
||||
ctrl.persistAppStatus(origApp, newStatus, newAnnotations)
|
||||
ctrl.persistAppStatus(t.Context(), origApp, newStatus, newAnnotations)
|
||||
|
||||
// Verify the patch was created correctly
|
||||
patchedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
|
||||
|
||||
+10
-2
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/gitops-engine/v3/pkg/sync/hook"
|
||||
"github.com/argoproj/argo-cd/gitops-engine/v3/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
argoutil "github.com/argoproj/argo-cd/v3/util/argo"
|
||||
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
|
||||
)
|
||||
|
||||
type HookType string
|
||||
@@ -94,7 +96,10 @@ func hasGitOpsEngineSyncPhaseHook(obj *unstructured.Unstructured) bool {
|
||||
}
|
||||
|
||||
// executeHooks is a generic function to execute hooks of a specified type
|
||||
func (ctrl *ApplicationController) executeHooks(ctx context.Context, hookType HookType, app *appv1.Application, proj *appv1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
|
||||
func (ctrl *ApplicationController) executeHooks(ctx context.Context, hookType HookType, app *appv1.Application, proj *appv1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (completed bool, retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "controller.executeHooks")
|
||||
setAppTraceAttrs(span, app, attribute.String("argocd.hook.type", string(hookType)))
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
appLabelKey, err := ctrl.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -236,7 +241,10 @@ func (ctrl *ApplicationController) executeHooks(ctx context.Context, hookType Ho
|
||||
}
|
||||
|
||||
// cleanupHooks is a generic function to clean up hooks of a specified type
|
||||
func (ctrl *ApplicationController) cleanupHooks(ctx context.Context, hookType HookType, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
|
||||
func (ctrl *ApplicationController) cleanupHooks(ctx context.Context, hookType HookType, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (completed bool, retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "controller.cleanupHooks")
|
||||
span.SetAttributes(attribute.String("argocd.hook.type", string(hookType)))
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -375,7 +375,7 @@ func (h *Hydrator) validateApplications(apps []*appv1.Application) (map[string]*
|
||||
}
|
||||
drySource := app.Spec.SourceHydrator.GetDrySource()
|
||||
if !proj.IsSourcePermitted(drySource) {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("application repo %s is not permitted in project '%s'", drySource.RepoURL, proj.Name)
|
||||
errors[app.QualifiedName()] = fmt.Errorf("application repo %s is not permitted in project '%s'", git.SanitizeRepoURL(drySource.RepoURL), proj.Name)
|
||||
continue
|
||||
}
|
||||
projects[app.Spec.Project] = proj
|
||||
@@ -392,7 +392,7 @@ func (h *Hydrator) validateApplications(apps []*appv1.Application) (map[string]*
|
||||
}
|
||||
|
||||
if !proj.IsSourcePermitted(hydrateToSource) {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("destination repo %s is not permitted in project '%s'", hydrateToSource.RepoURL, proj.Name)
|
||||
errors[app.QualifiedName()] = fmt.Errorf("destination repo %s is not permitted in project '%s'", git.SanitizeRepoURL(hydrateToSource.RepoURL), proj.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -400,8 +400,8 @@ func (h *Hydrator) validateApplications(apps []*appv1.Application) (map[string]*
|
||||
// TODO: normalize the path to avoid "path/.." from being treated as different from "."
|
||||
destKey := hydrationDestKey{repoURL: hydrateToSource.RepoURL, path: destPath}
|
||||
if appName, ok := uniquePaths[destKey]; ok {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("app %s hydrator uses the same destination: repo=%s, path=%s", appName, destKey.repoURL, destKey.path)
|
||||
errors[appName] = fmt.Errorf("app %s hydrator uses the same destination: repo=%s, path=%s", app.QualifiedName(), destKey.repoURL, destKey.path)
|
||||
errors[app.QualifiedName()] = fmt.Errorf("app %s hydrator uses the same destination: repo=%s, path=%s", appName, git.SanitizeRepoURL(destKey.repoURL), destKey.path)
|
||||
errors[appName] = fmt.Errorf("app %s hydrator uses the same destination: repo=%s, path=%s", app.QualifiedName(), git.SanitizeRepoURL(destKey.repoURL), destKey.path)
|
||||
continue
|
||||
}
|
||||
uniquePaths[destKey] = app.QualifiedName()
|
||||
@@ -596,7 +596,7 @@ func (h *Hydrator) getManifests(ctx context.Context, app *appv1.Application, tar
|
||||
func (h *Hydrator) getRevisionMetadata(ctx context.Context, repoURL, project, revision string) (*appv1.RevisionMetadata, error) {
|
||||
repo, err := h.repoGetter.GetRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get repository %q: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("failed to get repository %q: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
closer, repoService, err := h.repoClientset.NewRepoServerClient()
|
||||
|
||||
@@ -105,7 +105,8 @@ func (ctrl *ApplicationController) PersistHydrationStatus(orig *appv1.Applicatio
|
||||
delete(newAnnotations, appv1.AnnotationKeyHydrate)
|
||||
status := orig.Status.DeepCopy()
|
||||
status.SourceHydrator = *newStatus
|
||||
ctrl.persistAppStatus(orig, status, newAnnotations)
|
||||
// PersistHydrationStatus has no request context; the status write roots its own trace.
|
||||
ctrl.persistAppStatus(context.Background(), orig, status, newAnnotations)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) AddHydrationQueueItem(key types.HydrationQueueKey) {
|
||||
|
||||
+141
-83
@@ -26,6 +26,9 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/sourceintegrity"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -44,13 +47,31 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
"github.com/argoproj/argo-cd/v3/util/stats"
|
||||
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
|
||||
)
|
||||
|
||||
var ErrCompareStateRepo = errors.New("failed to get repo objects")
|
||||
|
||||
var tracer = otel.Tracer("github.com/argoproj/argo-cd/v3/controller")
|
||||
|
||||
// setAppTraceAttrs sets the standard argocd.app.* span attributes (plus any extra attributes)
|
||||
// on span. It is a no-op when the span is not recording, so callers on hot reconcile paths do
|
||||
// not allocate attribute slices when tracing is disabled.
|
||||
func setAppTraceAttrs(span oteltrace.Span, app *v1alpha1.Application, extra ...attribute.KeyValue) {
|
||||
if !span.IsRecording() {
|
||||
return
|
||||
}
|
||||
span.SetAttributes(append([]attribute.KeyValue{
|
||||
attribute.String("argocd.app.name", app.Name),
|
||||
attribute.String("argocd.app.namespace", app.Namespace),
|
||||
attribute.String("argocd.app.project", app.Spec.GetProject()),
|
||||
}, extra...)...)
|
||||
}
|
||||
|
||||
type resourceInfoProviderStub struct{}
|
||||
|
||||
func (r *resourceInfoProviderStub) IsNamespaced(_ schema.GroupKind) (bool, error) {
|
||||
@@ -204,7 +225,12 @@ func (m *appStateManager) EvaluateAppRevisionsChanges(ctx context.Context, app *
|
||||
// task to the repo-server. It returns the list of generated manifests as unstructured
|
||||
// objects. It also returns the full response from all calls to the repo server as the
|
||||
// second argument.
|
||||
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache bool, sourceIntegrity *v1alpha1.SourceIntegrity, proj *v1alpha1.AppProject, sendRuntimeState bool) (_ []*unstructured.Unstructured, _ []*apiclient.ManifestResponse, _ bool, retErr error) {
|
||||
// This span is the parent the otelgrpc client handler propagates onto the repo-server
|
||||
// GenerateManifest RPCs below, joining the repo-server trace to this reconcile.
|
||||
ctx, span := tracer.Start(ctx, "controller.GetRepoObjs")
|
||||
setAppTraceAttrs(span, app)
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(ctx)
|
||||
if err != nil {
|
||||
@@ -314,90 +340,111 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
|
||||
}
|
||||
revision := revisions[i]
|
||||
|
||||
// Use evaluateRevisionChanges to check for changes and get resolved revision
|
||||
resolvedRevision, hasChanges, err := m.evaluateRevisionChanges(ctx, app, source, i, revision, refSources, syncedRefSources, noRevisionCache, trackingMethod, appLabelKey, installationID, serverVersion, apiVersions, proj, repoClient)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to evaluate revision changes for source %d of %d: %w", i+1, len(sources), err)
|
||||
// Per-source span so the repo-server hop is attributed: a multi-source app otherwise
|
||||
// produces one parent span plus N anonymous GenerateManifest RPC spans with nothing
|
||||
// indicating which source each belongs to. The closure scopes srcCtx (the parent the
|
||||
// otelgrpc client handler propagates onto the RPC) and ends the span per iteration.
|
||||
srcCtx, srcSpan := tracer.Start(ctx, "controller.GetRepoObjs.source")
|
||||
if srcSpan.IsRecording() {
|
||||
srcSpan.SetAttributes(
|
||||
attribute.Int("argocd.source.index", i),
|
||||
attribute.String("argocd.source.name", source.Name),
|
||||
attribute.String("argocd.source.repo_url", git.SanitizeRepoURL(source.RepoURL)),
|
||||
attribute.String("argocd.revision", revision),
|
||||
)
|
||||
}
|
||||
if err := func() (retErr error) {
|
||||
defer func() { traceutil.EndSpan(srcSpan, retErr) }()
|
||||
|
||||
if hasChanges {
|
||||
revisionsMayHaveChanges = true
|
||||
}
|
||||
|
||||
// Use the resolved revision from evaluateRevisionChanges
|
||||
revision = resolvedRevision
|
||||
revisions[i] = resolvedRevision
|
||||
|
||||
appNamespace := app.Spec.Destination.Namespace
|
||||
|
||||
repos := permittedHelmRepos
|
||||
helmRepoCreds := permittedHelmCredentials
|
||||
// If the source is OCI, there is a potential for an OCI image to be a Helm chart and that said chart in
|
||||
// turn would have OCI dependencies. To ensure that those dependencies can be resolved, add them to the repos
|
||||
// list.
|
||||
if source.IsOCI() {
|
||||
repos = slices.Clone(permittedHelmRepos)
|
||||
helmRepoCreds = slices.Clone(permittedHelmCredentials)
|
||||
repos = append(repos, permittedOCIRepos...)
|
||||
helmRepoCreds = append(helmRepoCreds, permittedOCICredentials...)
|
||||
}
|
||||
|
||||
repo, err := m.db.GetRepository(ctx, source.RepoURL, proj.Name)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get repo %q: %w", source.RepoURL, err)
|
||||
}
|
||||
|
||||
log.Debugf("Generating Manifest for source %s revision %s", source, revision)
|
||||
manifestInfo, err := repoClient.GenerateManifest(ctx, &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: repos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.InstanceName(m.namespace),
|
||||
Namespace: appNamespace,
|
||||
ApplicationSource: &source,
|
||||
KustomizeOptions: kustomizeSettings,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: apiVersions,
|
||||
SourceIntegrity: sourceIntegrity,
|
||||
VerifySignature: sourceIntegrity != nil, // nolint:staticcheck
|
||||
HelmRepoCreds: helmRepoCreds,
|
||||
TrackingMethod: trackingMethod,
|
||||
EnabledSourceTypes: enabledSourceTypes,
|
||||
HelmOptions: helmOptions,
|
||||
HasMultipleSources: app.Spec.HasMultipleSources(),
|
||||
RefSources: refSources,
|
||||
ProjectName: proj.Name,
|
||||
ProjectSourceRepos: proj.Spec.SourceRepos,
|
||||
AnnotationManifestGeneratePaths: app.GetAnnotation(v1alpha1.AnnotationKeyManifestGeneratePaths),
|
||||
InstallationID: installationID,
|
||||
})
|
||||
if err != nil {
|
||||
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, len(sources), err)
|
||||
if app.Spec.SourceHydrator != nil && app.Spec.SourceHydrator.HydrateTo != nil && strings.Contains(err.Error(), path.ErrMessageAppPathDoesNotExist) {
|
||||
genErr = fmt.Errorf("%w - waiting for an external process to update %s from %s", genErr, app.Spec.SourceHydrator.SyncSource.TargetBranch, app.Spec.SourceHydrator.HydrateTo.TargetBranch)
|
||||
// Use evaluateRevisionChanges to check for changes and get resolved revision
|
||||
resolvedRevision, hasChanges, err := m.evaluateRevisionChanges(srcCtx, app, source, i, revision, refSources, syncedRefSources, noRevisionCache, trackingMethod, appLabelKey, installationID, serverVersion, apiVersions, proj, repoClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to evaluate revision changes for source %d of %d: %w", i+1, len(sources), err)
|
||||
}
|
||||
return nil, nil, false, genErr
|
||||
}
|
||||
|
||||
targetObj, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, len(sources), err)
|
||||
}
|
||||
targetObjs = append(targetObjs, targetObj...)
|
||||
manifestInfos = append(manifestInfos, manifestInfo)
|
||||
|
||||
// Update eventual check problems with the ID of the current source. This is so users can attribute problems to correct sources
|
||||
if len(sources) > 1 {
|
||||
var sourceId string
|
||||
if source.Name != "" {
|
||||
sourceId = "source " + source.Name
|
||||
} else {
|
||||
sourceId = fmt.Sprintf("source %d of %d", i+1, len(sources))
|
||||
if hasChanges {
|
||||
revisionsMayHaveChanges = true
|
||||
}
|
||||
manifestInfo.SourceIntegrityResult.InjectSourceName(sourceId)
|
||||
|
||||
// Use the resolved revision from evaluateRevisionChanges
|
||||
revision = resolvedRevision
|
||||
revisions[i] = resolvedRevision
|
||||
srcSpan.SetAttributes(attribute.String("argocd.resolved_revision", revision))
|
||||
|
||||
appNamespace := app.Spec.Destination.Namespace
|
||||
|
||||
repos := permittedHelmRepos
|
||||
helmRepoCreds := permittedHelmCredentials
|
||||
// If the source is OCI, there is a potential for an OCI image to be a Helm chart and that said chart in
|
||||
// turn would have OCI dependencies. To ensure that those dependencies can be resolved, add them to the repos
|
||||
// list.
|
||||
if source.IsOCI() {
|
||||
repos = slices.Clone(permittedHelmRepos)
|
||||
helmRepoCreds = slices.Clone(permittedHelmCredentials)
|
||||
repos = append(repos, permittedOCIRepos...)
|
||||
helmRepoCreds = append(helmRepoCreds, permittedOCICredentials...)
|
||||
}
|
||||
|
||||
repo, err := m.db.GetRepository(srcCtx, source.RepoURL, proj.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get repo %q: %w", git.SanitizeRepoURL(source.RepoURL), err)
|
||||
}
|
||||
|
||||
log.Debugf("Generating Manifest for source %s revision %s", source, revision)
|
||||
manifestInfo, err := repoClient.GenerateManifest(srcCtx, &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: repos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.InstanceName(m.namespace),
|
||||
Namespace: appNamespace,
|
||||
ApplicationSource: &source,
|
||||
KustomizeOptions: kustomizeSettings,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: apiVersions,
|
||||
SourceIntegrity: sourceIntegrity,
|
||||
VerifySignature: sourceIntegrity != nil, // nolint:staticcheck
|
||||
HelmRepoCreds: helmRepoCreds,
|
||||
TrackingMethod: trackingMethod,
|
||||
EnabledSourceTypes: enabledSourceTypes,
|
||||
HelmOptions: helmOptions,
|
||||
HasMultipleSources: app.Spec.HasMultipleSources(),
|
||||
RefSources: refSources,
|
||||
ProjectName: proj.Name,
|
||||
ProjectSourceRepos: proj.Spec.SourceRepos,
|
||||
AnnotationManifestGeneratePaths: app.GetAnnotation(v1alpha1.AnnotationKeyManifestGeneratePaths),
|
||||
InstallationID: installationID,
|
||||
})
|
||||
if err != nil {
|
||||
genErr := fmt.Errorf("failed to generate manifest for source %d of %d: %w", i+1, len(sources), err)
|
||||
if app.Spec.SourceHydrator != nil && app.Spec.SourceHydrator.HydrateTo != nil && strings.Contains(err.Error(), path.ErrMessageAppPathDoesNotExist) {
|
||||
genErr = fmt.Errorf("%w - waiting for an external process to update %s from %s", genErr, app.Spec.SourceHydrator.SyncSource.TargetBranch, app.Spec.SourceHydrator.HydrateTo.TargetBranch)
|
||||
}
|
||||
return genErr
|
||||
}
|
||||
|
||||
targetObj, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal manifests for source %d of %d: %w", i+1, len(sources), err)
|
||||
}
|
||||
targetObjs = append(targetObjs, targetObj...)
|
||||
manifestInfos = append(manifestInfos, manifestInfo)
|
||||
|
||||
// Update eventual check problems with the ID of the current source. This is so users can attribute problems to correct sources
|
||||
if len(sources) > 1 {
|
||||
var sourceId string
|
||||
if source.Name != "" {
|
||||
sourceId = "source " + source.Name
|
||||
} else {
|
||||
sourceId = fmt.Sprintf("source %d of %d", i+1, len(sources))
|
||||
}
|
||||
manifestInfo.SourceIntegrityResult.InjectSourceName(sourceId)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
return nil, nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +499,7 @@ func (m *appStateManager) evaluateRevisionChanges(ctx context.Context, app *v1al
|
||||
}
|
||||
repo, err := m.db.GetRepository(ctx, source.RepoURL, proj.Name)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("failed to get repo %q: %w", source.RepoURL, err)
|
||||
return "", false, fmt.Errorf("failed to get repo %q: %w", git.SanitizeRepoURL(source.RepoURL), err)
|
||||
}
|
||||
|
||||
keyManifestGenerateAnnotationVal := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]
|
||||
@@ -630,7 +677,10 @@ func partitionTargetObjsForSync(targetObjs []*unstructured.Unstructured) (syncOb
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (m *appStateManager) CompareAppState(ctx context.Context, app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool) (*comparisonResult, error) {
|
||||
func (m *appStateManager) CompareAppState(ctx context.Context, app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool) (_ *comparisonResult, retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "controller.CompareAppState")
|
||||
setAppTraceAttrs(span, app)
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
ts := stats.NewTimingStats()
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
|
||||
@@ -915,7 +965,15 @@ func (m *appStateManager) CompareAppState(ctx context.Context, app *v1alpha1.App
|
||||
// application conditions as argo.StateDiffs will validate this diffConfig again.
|
||||
diffConfig, _ := diffConfigBuilder.Build()
|
||||
|
||||
diffResults, err := argodiff.StateDiffs(ctx, reconciliation.Live, reconciliation.Target, diffConfig)
|
||||
// Scope the diff span with a closure so it ends even if StateDiffs panics, while keeping
|
||||
// it attributed to just the diff rather than the rest of CompareAppState.
|
||||
var diffResults *diff.DiffResultList
|
||||
err = func() (retErr error) {
|
||||
_, diffSpan := tracer.Start(ctx, "controller.diff")
|
||||
defer func() { traceutil.EndSpan(diffSpan, retErr) }()
|
||||
diffResults, retErr = argodiff.StateDiffs(ctx, reconciliation.Live, reconciliation.Target, diffConfig)
|
||||
return retErr
|
||||
}()
|
||||
if err != nil {
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/gitops-engine/v3/pkg/utils/kube"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
otel_codes "go.opentelemetry.io/otel/codes"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -110,6 +111,16 @@ func newSyncOperationResult(app *v1alpha1.Application, op v1alpha1.SyncOperation
|
||||
}
|
||||
|
||||
func (m *appStateManager) SyncAppState(ctx context.Context, app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState) {
|
||||
ctx, span := tracer.Start(ctx, "controller.SyncAppState")
|
||||
setAppTraceAttrs(span, app)
|
||||
// SyncAppState is void; it signals failure through state.Phase rather than a return value, so
|
||||
// map a terminal failed phase onto the span status at exit (mirroring traceutil.EndSpan).
|
||||
defer func() {
|
||||
if state.Phase.Failed() {
|
||||
span.SetStatus(otel_codes.Error, state.Message)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
syncId, err := syncid.Generate()
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
|
||||
@@ -32,6 +32,8 @@ data:
|
||||
otlp.headers: ""
|
||||
# Open-Telemetry collector attrs: (e.g. "key1:value1,key2:value2")
|
||||
otlp.attrs: ""
|
||||
# Open-Telemetry trace sampling ratio between 0.0 and 1.0: (e.g. "0.1")
|
||||
otlp.sample.ratio: "1.0"
|
||||
|
||||
# List of additional namespaces where applications may be created in and
|
||||
# reconciled from. The namespace where Argo CD is installed to will always
|
||||
|
||||
@@ -54,6 +54,7 @@ argocd-application-controller [flags]
|
||||
--otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)
|
||||
--otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default [])
|
||||
--otlp-insecure OpenTelemetry collector insecure mode (default true)
|
||||
--otlp-sample-ratio float Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision (default 1)
|
||||
--password string Password for basic authentication to the API server
|
||||
--persist-resource-health Enables storing the managed resources health in the Application CRD
|
||||
--proxy-url string If provided, this URL will be used to connect via proxy
|
||||
|
||||
@@ -38,6 +38,7 @@ argocd-repo-server [flags]
|
||||
--otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)
|
||||
--otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default [])
|
||||
--otlp-insecure OpenTelemetry collector insecure mode (default true)
|
||||
--otlp-sample-ratio float Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision (default 1)
|
||||
--parallelismlimit int Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.
|
||||
--plugin-tar-exclude stringArray Globs to filter when sending tarballs to plugins.
|
||||
--plugin-use-manifest-generate-paths Pass the resources described in argocd.argoproj.io/manifest-generate-paths value to the cmpserver to generate the application manifests.
|
||||
|
||||
@@ -72,6 +72,7 @@ argocd-server [flags]
|
||||
--otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)
|
||||
--otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default [])
|
||||
--otlp-insecure OpenTelemetry collector insecure mode (default true)
|
||||
--otlp-sample-ratio float Fraction of traces to sample, from 0.0 (none) to 1.0 (all). Parent-based, so downstream services honor the upstream sampling decision (default 1)
|
||||
--password string Password for basic authentication to the API server
|
||||
--port int Listen on given port (default 8080)
|
||||
--proxy-url string If provided, this URL will be used to connect via proxy
|
||||
|
||||
@@ -24,3 +24,37 @@ Up to three resources are listed. This makes it easier to investigate flapping a
|
||||
**Impact:**
|
||||
|
||||
- If you consume this event message and parse it programmatically, update your parser to tolerate the new `(Caused by ...)` suffix.
|
||||
|
||||
## Tracing sampler is now parent-based and configurable
|
||||
|
||||
Previously, when OpenTelemetry tracing was enabled (`--otlp-address` set), Argo CD
|
||||
sampled **every** span unconditionally (`AlwaysSample`). Argo CD now uses a
|
||||
parent-based head sampler whose root sampling rate is configurable per component via
|
||||
the `--otlp-sample-ratio` flag (or the `ARGOCD_<COMPONENT>_OTLP_SAMPLE_RATIO`
|
||||
environment variable). The flag defaults to `1.0` (sample every trace), so for most
|
||||
deployments behaviour is unchanged.
|
||||
|
||||
There are two behavioural differences to be aware of:
|
||||
|
||||
> [!NOTE]
|
||||
> **Sampling is now propagated across services.** A sampling decision made by the
|
||||
> service that starts a trace (for example the application controller) is honored by
|
||||
> every downstream service the trace context flows into (repo-server, commit-server).
|
||||
> This keeps each trace whole instead of being partially sampled across process
|
||||
> boundaries. Set the same `--otlp-sample-ratio` on all components to get a consistent
|
||||
> rate.
|
||||
|
||||
> [!WARNING]
|
||||
> **An inbound "not sampled" decision is now honored.** Because the sampler is
|
||||
> parent-based, a request that already carries a W3C `traceparent` marked *not sampled*
|
||||
> is no longer recorded, even with `--otlp-sample-ratio=1.0`. Under the old
|
||||
> `AlwaysSample` behaviour such requests were recorded regardless. This only affects
|
||||
> deployments where an upstream proxy, gateway, or client injects trace context into
|
||||
> requests to `argocd-server`; if nothing upstream propagates `traceparent`, every
|
||||
> trace is still rooted in Argo CD and sampled at the configured ratio.
|
||||
|
||||
Values supplied to `--otlp-sample-ratio` outside the `[0.0, 1.0]` range (or `NaN`) are
|
||||
**rejected at startup** with an error, so a misconfigured flag fails fast rather than
|
||||
silently disabling or exceeding sampling. When the ratio is set via the
|
||||
`ARGOCD_<COMPONENT>_OTLP_SAMPLE_RATIO` environment variable instead, an invalid or
|
||||
out-of-range value is ignored and the default (`1.0`) is used (a warning is logged).
|
||||
|
||||
@@ -10,6 +10,7 @@ require (
|
||||
github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
go.opentelemetry.io/otel v1.41.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
|
||||
k8s.io/api v0.36.1
|
||||
@@ -39,6 +40,7 @@ require (
|
||||
github.com/fatih/camelcase v1.0.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
@@ -68,7 +70,8 @@ require (
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
|
||||
@@ -33,8 +33,11 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
@@ -134,8 +137,12 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
|
||||
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
otel_codes "go.opentelemetry.io/otel/codes"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
@@ -36,6 +39,18 @@ import (
|
||||
kubeutil "github.com/argoproj/argo-cd/gitops-engine/v3/pkg/utils/kube"
|
||||
)
|
||||
|
||||
var tracer = otel.Tracer("github.com/argoproj/argo-cd/gitops-engine/pkg/sync")
|
||||
|
||||
// taskTraceAttrs returns the standard argocd.resource.* span attributes for a sync task.
|
||||
func taskTraceAttrs(t *syncTask) []attribute.KeyValue {
|
||||
return []attribute.KeyValue{
|
||||
attribute.String("argocd.resource.group", t.group()),
|
||||
attribute.String("argocd.resource.kind", t.kind()),
|
||||
attribute.String("argocd.resource.namespace", t.namespace()),
|
||||
attribute.String("argocd.resource.name", t.name()),
|
||||
}
|
||||
}
|
||||
|
||||
type reconciledResource struct {
|
||||
Target *unstructured.Unstructured
|
||||
Live *unstructured.Unstructured
|
||||
@@ -476,6 +491,9 @@ func (sc *syncContext) setRunningPhase(tasks syncTasks, isPendingDeletion bool)
|
||||
|
||||
// Sync executes next synchronization step and updates operation status.
|
||||
func (sc *syncContext) Sync(ctx context.Context) {
|
||||
ctx, span := tracer.Start(ctx, "sync.Sync")
|
||||
span.SetAttributes(attribute.Bool("argocd.sync.started", sc.started()))
|
||||
defer span.End()
|
||||
sc.log.WithValues("skipHooks", sc.skipHooks, "started", sc.started()).Info("Syncing")
|
||||
tasks, ok := sc.getSyncTasks(ctx)
|
||||
if !ok {
|
||||
@@ -723,6 +741,8 @@ func (sc *syncContext) Sync(ctx context.Context) {
|
||||
// Terminate terminates sync operation. The method is asynchronous: it starts deletion is related K8S resources
|
||||
// such as in-flight resource hooks, updates operation status, and exists without waiting for resource completion.
|
||||
func (sc *syncContext) Terminate(ctx context.Context) {
|
||||
ctx, span := tracer.Start(ctx, "sync.Terminate")
|
||||
defer span.End()
|
||||
sc.log.V(1).Info("terminating")
|
||||
tasks, _ := sc.getSyncTasks(ctx)
|
||||
|
||||
@@ -952,6 +972,13 @@ func (sc *syncContext) containsResource(resource reconciledResource) bool {
|
||||
|
||||
// generates the list of sync tasks we will be performing during this sync.
|
||||
func (sc *syncContext) getSyncTasks(ctx context.Context) (_ syncTasks, successful bool) {
|
||||
ctx, span := tracer.Start(ctx, "sync.getSyncTasks")
|
||||
defer func() {
|
||||
if !successful {
|
||||
span.SetStatus(otel_codes.Error, "one or more sync tasks failed to generate")
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
resourceTasks := syncTasks{}
|
||||
successful = true
|
||||
|
||||
@@ -1387,6 +1414,13 @@ func (sc *syncContext) performCSAUpgradeMigration(ctx context.Context, liveObj *
|
||||
}
|
||||
|
||||
func (sc *syncContext) applyObject(ctx context.Context, t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
|
||||
ctx, span := tracer.Start(ctx, "sync.apply")
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(taskTraceAttrs(t)...)
|
||||
span.SetAttributes(attribute.Bool("argocd.sync.dry_run", dryRun))
|
||||
}
|
||||
defer span.End()
|
||||
|
||||
dryRunStrategy := cmdutil.DryRunNone
|
||||
if dryRun {
|
||||
// irrespective of the dry run mode set in the sync context, always run
|
||||
@@ -1451,7 +1485,15 @@ func (sc *syncContext) applyObject(ctx context.Context, t *syncTask, dryRun, val
|
||||
}
|
||||
|
||||
// pruneObject deletes the object if both prune is true and dryRun is false. Otherwise appropriate message
|
||||
func (sc *syncContext) pruneObject(ctx context.Context, liveObj *unstructured.Unstructured, prune, dryRun bool) (common.ResultCode, string) {
|
||||
func (sc *syncContext) pruneObject(ctx context.Context, t *syncTask, prune, dryRun bool) (common.ResultCode, string) {
|
||||
ctx, span := tracer.Start(ctx, "sync.prune")
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(taskTraceAttrs(t)...)
|
||||
span.SetAttributes(attribute.Bool("argocd.sync.dry_run", dryRun))
|
||||
}
|
||||
defer span.End()
|
||||
|
||||
liveObj := t.liveObj
|
||||
if !prune {
|
||||
return common.ResultCodePruneSkipped, "ignored (requires pruning)"
|
||||
} else if isPruningDisabled(liveObj, sc.defaultPruneOption) {
|
||||
@@ -1584,6 +1626,15 @@ const (
|
||||
func (sc *syncContext) runTasks(ctx context.Context, tasks syncTasks, dryRun bool) runState {
|
||||
dryRun = dryRun || sc.dryRun
|
||||
|
||||
ctx, span := tracer.Start(ctx, "sync.runTasks")
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(
|
||||
attribute.Bool("argocd.sync.dry_run", dryRun),
|
||||
attribute.Int("argocd.sync.num_tasks", len(tasks)),
|
||||
)
|
||||
}
|
||||
defer span.End()
|
||||
|
||||
sc.log.WithValues("numTasks", len(tasks), "dryRun", dryRun).V(1).Info("Running tasks")
|
||||
|
||||
state := successful
|
||||
@@ -1630,7 +1681,7 @@ func (sc *syncContext) runTasks(ctx context.Context, tasks syncTasks, dryRun boo
|
||||
ss.Go(func(state runState) runState {
|
||||
logCtx := sc.log.WithValues("dryRun", dryRun, "task", t)
|
||||
logCtx.V(1).Info("Pruning")
|
||||
result, message := sc.pruneObject(ctx, t.liveObj, sc.prune, dryRun)
|
||||
result, message := sc.pruneObject(ctx, t, sc.prune, dryRun)
|
||||
if result == common.ResultCodeSyncFailed {
|
||||
state = failed
|
||||
logCtx.WithValues("message", message).Info("Pruning failed")
|
||||
|
||||
+6
@@ -280,6 +280,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.headers
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.sample.ratio
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
@@ -324,6 +324,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.attrs
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.sample.ratio
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
@@ -181,6 +181,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.attrs
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.sample.ratio
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
@@ -318,6 +318,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.attrs
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: otlp.sample.ratio
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
+12
@@ -32172,6 +32172,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -32749,6 +32755,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+12
@@ -32000,6 +32000,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -32577,6 +32583,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -33917,6 +33917,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -34486,6 +34492,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -35013,6 +35025,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -33747,6 +33747,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -34316,6 +34322,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -34843,6 +34855,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
+18
@@ -2831,6 +2831,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -3400,6 +3406,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -3927,6 +3939,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -2661,6 +2661,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -3230,6 +3236,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -3757,6 +3769,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -32887,6 +32887,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -33454,6 +33460,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -33981,6 +33993,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -32715,6 +32715,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -33282,6 +33288,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -33809,6 +33821,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
+18
@@ -1801,6 +1801,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -2368,6 +2374,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -2895,6 +2907,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
Generated
+18
@@ -1629,6 +1629,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -2196,6 +2202,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_SERVER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
@@ -2723,6 +2735,12 @@ spec:
|
||||
key: otlp.attrs
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_CONTROLLER_OTLP_SAMPLE_RATIO
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: otlp.sample.ratio
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_APPLICATION_NAMESPACES
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
@@ -38,6 +38,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -69,6 +71,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/util/kustomize"
|
||||
"github.com/argoproj/argo-cd/v3/util/manifeststream"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
traceutil "github.com/argoproj/argo-cd/v3/util/trace"
|
||||
"github.com/argoproj/argo-cd/v3/util/versions"
|
||||
)
|
||||
|
||||
@@ -83,6 +86,8 @@ const (
|
||||
|
||||
var ErrExceededMaxCombinedManifestFileSize = errors.New("exceeded max combined manifest file size")
|
||||
|
||||
var tracer = otel.Tracer("github.com/argoproj/argo-cd/v3/reposerver/repository")
|
||||
|
||||
// Service implements ManifestService interface
|
||||
type Service struct {
|
||||
gitCredsStore git.CredsStore
|
||||
@@ -637,8 +642,21 @@ func (s *Service) checkOutOfBoundsSymlinks(rootPath string, version string, noCa
|
||||
return checker.(func() error)()
|
||||
}
|
||||
|
||||
func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
|
||||
var res *apiclient.ManifestResponse
|
||||
func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (res *apiclient.ManifestResponse, retErr error) {
|
||||
// The otelgrpc server handler already created the RPC span as the parent; this names
|
||||
// and annotates the manifest-generation work so it shows up in the reconcile trace.
|
||||
ctx, span := tracer.Start(ctx, "reposerver.GenerateManifest")
|
||||
if span.IsRecording() {
|
||||
span.SetAttributes(
|
||||
attribute.String("argocd.app.name", q.AppName),
|
||||
attribute.String("argocd.revision", q.Revision),
|
||||
)
|
||||
if q.Repo != nil {
|
||||
span.SetAttributes(attribute.String("argocd.repo.url", git.SanitizeRepoURL(q.Repo.Repo)))
|
||||
}
|
||||
}
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
|
||||
var err error
|
||||
|
||||
// Skip this path for ref only sources
|
||||
@@ -1726,7 +1744,10 @@ func WithCMPUseManifestGeneratePaths(enabled bool) GenerateManifestOpt {
|
||||
}
|
||||
|
||||
// GenerateManifests generates manifests from a path. Overrides are applied as a side effect on the given ApplicationSource.
|
||||
func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore, maxCombinedManifestQuantity resource.Quantity, gitRepoPaths utilio.TempPaths, opts ...GenerateManifestOpt) (*apiclient.ManifestResponse, error) {
|
||||
func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore, maxCombinedManifestQuantity resource.Quantity, gitRepoPaths utilio.TempPaths, opts ...GenerateManifestOpt) (_ *apiclient.ManifestResponse, retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "reposerver.GenerateManifests")
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
|
||||
opt := newGenerateManifestOpt(opts...)
|
||||
var targetObjs []*unstructured.Unstructured
|
||||
|
||||
@@ -1738,6 +1759,7 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting app source type: %w", err)
|
||||
}
|
||||
span.SetAttributes(attribute.String("argocd.app.source_type", string(appSourceType)))
|
||||
repoURL := ""
|
||||
if q.Repo != nil {
|
||||
repoURL = q.Repo.Repo
|
||||
@@ -2955,7 +2977,10 @@ func directoryPermissionInitializer(rootPath string) goio.Closer {
|
||||
|
||||
// checkoutRevision is a convenience function to initialize a repo, fetch, and checkout a revision
|
||||
// Returns the 40 character commit SHA after the checkout has been performed
|
||||
func (s *Service) checkoutRevision(ctx context.Context, gitClient git.Client, revision string, submoduleEnabled bool, depth int64, clean bool) (goio.Closer, error) {
|
||||
func (s *Service) checkoutRevision(ctx context.Context, gitClient git.Client, revision string, submoduleEnabled bool, depth int64, clean bool) (_ goio.Closer, retErr error) {
|
||||
ctx, span := tracer.Start(ctx, "reposerver.checkoutRevision")
|
||||
span.SetAttributes(attribute.String("argocd.revision", revision))
|
||||
defer func() { traceutil.EndSpan(span, retErr) }()
|
||||
closer := s.gitRepoInitializer(gitClient.Root())
|
||||
err := checkoutRevision(ctx, gitClient, revision, submoduleEnabled, depth, clean)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@ import (
|
||||
applicationsv1 "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/util/db"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/glob"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
@@ -568,7 +569,7 @@ func GetRefSources(ctx context.Context, sources argoappv1.ApplicationSources, pr
|
||||
|
||||
repo, err := getRepository(ctx, source.RepoURL, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get repository %s: %w", source.RepoURL, err)
|
||||
return nil, fmt.Errorf("failed to get repository %s: %w", git.SanitizeRepoURL(source.RepoURL), err)
|
||||
}
|
||||
refKey := "$" + source.Ref
|
||||
revision := source.TargetRevision
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
stderrors "errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
@@ -349,3 +350,35 @@ func PrintDiff(name string, live *unstructured.Unstructured, target *unstructure
|
||||
cmd.Stdout = os.Stdout
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// boundedFloat64Value is a pflag.Value that accepts a float64 within [min, max].
|
||||
type boundedFloat64Value struct {
|
||||
val *float64
|
||||
min, max float64
|
||||
}
|
||||
|
||||
func (b *boundedFloat64Value) Set(s string) error {
|
||||
v, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// NaN must be rejected explicitly: NaN comparisons are always false, so it would
|
||||
// otherwise slip past the range check below.
|
||||
if math.IsNaN(v) || v < b.min || v > b.max {
|
||||
return fmt.Errorf("%s is out of range [%g, %g]", s, b.min, b.max)
|
||||
}
|
||||
*b.val = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*boundedFloat64Value) Type() string { return "float" }
|
||||
|
||||
func (b *boundedFloat64Value) String() string { return strconv.FormatFloat(*b.val, 'g', -1, 64) }
|
||||
|
||||
// BoundedFloat64Var defines a float64 flag constrained to [min, max], rejecting out-of-range
|
||||
// or NaN input at parse time. It mirrors the signature of pflag's Float64Var. The default
|
||||
// value is not validated, matching pflag's behavior (Set runs only for an explicit flag).
|
||||
func BoundedFloat64Var(fs *pflag.FlagSet, p *float64, name string, value, minimum, maximum float64, usage string) {
|
||||
*p = value
|
||||
fs.Var(&boundedFloat64Value{val: p, min: minimum, max: maximum}, name, usage)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package cli
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
@@ -133,3 +135,42 @@ func TestSetGLogLevel(t *testing.T) {
|
||||
logToStderrFlag := flag.Lookup("logtostderr")
|
||||
assert.Equal(t, "true", logToStderrFlag.Value.String())
|
||||
}
|
||||
|
||||
func TestBoundedFloat64Var(t *testing.T) {
|
||||
newFlagSet := func() (*pflag.FlagSet, *float64) {
|
||||
var v float64
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
fs.SetOutput(io.Discard)
|
||||
BoundedFloat64Var(fs, &v, "ratio", 1.0, 0.0, 1.0, "usage")
|
||||
return fs, &v
|
||||
}
|
||||
|
||||
t.Run("default applied when flag absent", func(t *testing.T) {
|
||||
fs, v := newFlagSet()
|
||||
require.NoError(t, fs.Parse(nil))
|
||||
assert.InDelta(t, 1.0, *v, 0)
|
||||
})
|
||||
|
||||
t.Run("type and default string for help/docs", func(t *testing.T) {
|
||||
fs, _ := newFlagSet()
|
||||
f := fs.Lookup("ratio")
|
||||
assert.Equal(t, "float", f.Value.Type())
|
||||
assert.Equal(t, "1", f.DefValue)
|
||||
})
|
||||
|
||||
valid := map[string]float64{"0": 0.0, "0.3": 0.3, "1": 1.0}
|
||||
for in, want := range valid {
|
||||
t.Run("accepts "+in, func(t *testing.T) {
|
||||
fs, v := newFlagSet()
|
||||
require.NoError(t, fs.Parse([]string{"--ratio=" + in}))
|
||||
assert.InDelta(t, want, *v, 0)
|
||||
})
|
||||
}
|
||||
|
||||
for _, in := range []string{"-0.5", "2", "NaN", "abc"} {
|
||||
t.Run("rejects "+in, func(t *testing.T) {
|
||||
fs, _ := newFlagSet()
|
||||
assert.Error(t, fs.Parse([]string{"--ratio=" + in}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
@@ -86,11 +87,11 @@ func (db *db) CreateWriteRepository(ctx context.Context, r *v1alpha1.Repository)
|
||||
func (db *db) GetRepository(ctx context.Context, repoURL, project string) (*v1alpha1.Repository, error) {
|
||||
repository, err := db.getRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return repository, fmt.Errorf("unable to get repository %q: %w", repoURL, err)
|
||||
return repository, fmt.Errorf("unable to get repository %q: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
if err := db.enrichCredsToRepo(ctx, repository); err != nil {
|
||||
return repository, fmt.Errorf("unable to enrich repository %q info with credentials: %w", repoURL, err)
|
||||
return repository, fmt.Errorf("unable to enrich repository %q info with credentials: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
return repository, err
|
||||
@@ -99,11 +100,11 @@ func (db *db) GetRepository(ctx context.Context, repoURL, project string) (*v1al
|
||||
func (db *db) GetWriteRepository(ctx context.Context, repoURL, project string) (*v1alpha1.Repository, error) {
|
||||
repository, err := db.repoWriteBackend().GetRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return repository, fmt.Errorf("unable to get write repository %q: %w", repoURL, err)
|
||||
return repository, fmt.Errorf("unable to get write repository %q: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
if err := db.enrichWriteCredsToRepo(ctx, repository); err != nil {
|
||||
return repository, fmt.Errorf("unable to enrich write repository %q info with credentials: %w", repoURL, err)
|
||||
return repository, fmt.Errorf("unable to enrich write repository %q info with credentials: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
return repository, err
|
||||
@@ -151,11 +152,11 @@ func (db *db) getRepository(ctx context.Context, repoURL, project string) (*v1al
|
||||
secretsBackend := db.repoBackend()
|
||||
exists, err := secretsBackend.RepositoryExists(ctx, repoURL, project, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check if repository %q exists from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to check if repository %q exists from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
} else if exists {
|
||||
repository, err := secretsBackend.GetRepository(ctx, repoURL, project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get repository %q from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to get repository %q from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
return repository, nil
|
||||
}
|
||||
@@ -238,7 +239,7 @@ func (db *db) DeleteRepository(ctx context.Context, repoURL, project string) err
|
||||
return secretsBackend.DeleteRepository(ctx, repoURL, project)
|
||||
}
|
||||
|
||||
return status.Errorf(codes.NotFound, "repo '%s' not found", repoURL)
|
||||
return status.Errorf(codes.NotFound, "repo '%s' not found", git.SanitizeRepoURL(repoURL))
|
||||
}
|
||||
|
||||
func (db *db) DeleteWriteRepository(ctx context.Context, repoURL, project string) error {
|
||||
@@ -249,7 +250,7 @@ func (db *db) DeleteWriteRepository(ctx context.Context, repoURL, project string
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return status.Errorf(codes.NotFound, "repo '%s' not found", repoURL)
|
||||
return status.Errorf(codes.NotFound, "repo '%s' not found", git.SanitizeRepoURL(repoURL))
|
||||
}
|
||||
|
||||
return secretsBackend.DeleteRepository(ctx, repoURL, project)
|
||||
@@ -279,11 +280,11 @@ func (db *db) GetRepositoryCredentials(ctx context.Context, repoURL string) (*v1
|
||||
secretsBackend := db.repoBackend()
|
||||
exists, err := secretsBackend.RepoCredsExists(ctx, repoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to check if repository credentials for %q exists from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to check if repository credentials for %q exists from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
} else if exists {
|
||||
creds, err := secretsBackend.GetRepoCreds(ctx, repoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get repository credentials for %q from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to get repository credentials for %q from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
return creds, nil
|
||||
}
|
||||
@@ -297,9 +298,9 @@ func (db *db) GetWriteRepositoryCredentials(ctx context.Context, repoURL string)
|
||||
creds, err := secretBackend.GetRepoCreds(ctx, repoURL)
|
||||
if err != nil {
|
||||
if creds == nil {
|
||||
return nil, fmt.Errorf("unable to check if repo write credentials for %q exists from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to check if repo write credentials for %q exists from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get repo write credentials for %q from secrets backend: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("unable to get repo write credentials for %q from secrets backend: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
if creds == nil { // to cover for not found. In that case both creds and err are nil
|
||||
return nil, nil
|
||||
|
||||
@@ -172,7 +172,7 @@ func (s *secretsRepositoryBackend) RepositoryExists(_ context.Context, repoURL,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("failed to get repository secret for %q: %w", repoURL, err)
|
||||
return false, fmt.Errorf("failed to get repository secret for %q: %w", git.SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
return secret != nil, nil
|
||||
@@ -595,7 +595,7 @@ func (s *secretsRepositoryBackend) getRepositorySecret(repoURL, project string,
|
||||
return foundSecret, nil
|
||||
}
|
||||
|
||||
return nil, status.Errorf(codes.NotFound, "repository %q not found", repoURL)
|
||||
return nil, status.Errorf(codes.NotFound, "repository %q not found", git.SanitizeRepoURL(repoURL))
|
||||
}
|
||||
|
||||
func (s *secretsRepositoryBackend) getRepoCredsSecret(repoURL string) (*corev1.Secret, error) {
|
||||
@@ -606,7 +606,7 @@ func (s *secretsRepositoryBackend) getRepoCredsSecret(repoURL string) (*corev1.S
|
||||
|
||||
index := s.getRepositoryCredentialIndex(secrets, repoURL)
|
||||
if index < 0 {
|
||||
return nil, status.Errorf(codes.NotFound, "repository credentials %q not found", repoURL)
|
||||
return nil, status.Errorf(codes.NotFound, "repository credentials %q not found", git.SanitizeRepoURL(repoURL))
|
||||
}
|
||||
|
||||
return secrets[index], nil
|
||||
|
||||
+4
-4
@@ -276,11 +276,11 @@ func NewClient(rawRepoURL string, creds Creds, insecure bool, enableLfs bool, pr
|
||||
r := regexp.MustCompile(`([/:])`)
|
||||
normalizedGitURL := NormalizeGitURL(rawRepoURL)
|
||||
if normalizedGitURL == "" {
|
||||
return nil, fmt.Errorf("repository %q cannot be initialized: %w", rawRepoURL, ErrInvalidRepoURL)
|
||||
return nil, fmt.Errorf("repository %q cannot be initialized: %w", SanitizeRepoURL(rawRepoURL), ErrInvalidRepoURL)
|
||||
}
|
||||
root := filepath.Join(os.TempDir(), r.ReplaceAllString(normalizedGitURL, "_"))
|
||||
if root == os.TempDir() {
|
||||
return nil, fmt.Errorf("repository %q cannot be initialized, because its root would be system temp at %s", rawRepoURL, root)
|
||||
return nil, fmt.Errorf("repository %q cannot be initialized, because its root would be system temp at %s", SanitizeRepoURL(rawRepoURL), root)
|
||||
}
|
||||
return NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, noProxy, opts...)
|
||||
}
|
||||
@@ -431,7 +431,7 @@ func buildSSHAuth(repoURL string, creds *SSHCreds) (transport.AuthMethod, error)
|
||||
// avoids handing back an AuthMethod with no host-key verification.
|
||||
// For the no-credentials path, newAuth catches this and lets go-git
|
||||
// fall back to its DefaultAuthBuilder.
|
||||
return nil, fmt.Errorf("could not set up SSH known hosts callback for %s: %w", repoURL, err)
|
||||
return nil, fmt.Errorf("could not set up SSH known hosts callback for %s: %w", SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
if creds == nil {
|
||||
@@ -1030,7 +1030,7 @@ func (m *nativeGitClient) RevisionMetadata(ctx context.Context, revision string)
|
||||
cmd.Stdin = strings.NewReader(message)
|
||||
out, err = m.runCmdOutput(cmd, runOpts{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to interpret trailers for revision %q in repo %q: %w", revision, m.repoURL, err)
|
||||
return nil, fmt.Errorf("failed to interpret trailers for revision %q in repo %q: %w", revision, SanitizeRepoURL(m.repoURL), err)
|
||||
}
|
||||
relatedCommits, _ := GetReferences(log.WithFields(log.Fields{"repo": m.repoURL, "revision": revision}), out)
|
||||
|
||||
|
||||
+5
-5
@@ -560,10 +560,10 @@ func (g GitHubAppCreds) getInstallationTransport() (*ghinstallation.Transport, e
|
||||
if installationID == 0 {
|
||||
org, err := ExtractOrgFromRepoURL(g.repoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract organization from repository URL %s for GitHub App installation discovery: %w", g.repoURL, err)
|
||||
return nil, fmt.Errorf("failed to extract organization from repository URL %s for GitHub App installation discovery: %w", SanitizeRepoURL(g.repoURL), err)
|
||||
}
|
||||
if org == "" {
|
||||
return nil, fmt.Errorf("could not extract organization from repository URL %s: the URL does not contain an organization/owner", g.repoURL)
|
||||
return nil, fmt.Errorf("could not extract organization from repository URL %s: the URL does not contain an organization/owner", SanitizeRepoURL(g.repoURL))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
@@ -800,7 +800,7 @@ func ExtractOrgFromRepoURL(repoURL string) (string, error) {
|
||||
// Use git-urls library to parse all Git URL formats
|
||||
parsed, err := giturls.Parse(repoURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse repository URL %q: %w", repoURL, err)
|
||||
return "", fmt.Errorf("failed to parse repository URL %q: %w", SanitizeRepoURL(repoURL), err)
|
||||
}
|
||||
|
||||
// Clean the path: remove leading/trailing slashes and .git suffix
|
||||
@@ -808,7 +808,7 @@ func ExtractOrgFromRepoURL(repoURL string) (string, error) {
|
||||
path = strings.TrimSuffix(path, ".git")
|
||||
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("repository URL %q does not contain a path", repoURL)
|
||||
return "", fmt.Errorf("repository URL %q does not contain a path", SanitizeRepoURL(repoURL))
|
||||
}
|
||||
|
||||
// Extract the first path component (organization/owner)
|
||||
@@ -821,7 +821,7 @@ func ExtractOrgFromRepoURL(repoURL string) (string, error) {
|
||||
|
||||
// If there's no slash, the entire path might be just the org (unusual but handle it)
|
||||
// This would fail validation later, but let's return it
|
||||
return "", fmt.Errorf("could not extract organization from repository URL %q: path %q does not contain org/repo format", repoURL, path)
|
||||
return "", fmt.Errorf("could not extract organization from repository URL %q: path %q does not contain org/repo format", SanitizeRepoURL(repoURL), path)
|
||||
}
|
||||
|
||||
var _ Creds = GoogleCloudCreds{}
|
||||
|
||||
+12
-1
@@ -39,7 +39,18 @@ func IsTruncatedCommitSHA(sha string) bool {
|
||||
return truncatedCommitSHARegex.MatchString(sha)
|
||||
}
|
||||
|
||||
// SameURL returns whether or not the two repository URLs are equivalent in location
|
||||
// SanitizeRepoURL strips any embedded userinfo (e.g. https://token@host/org/repo) from a
|
||||
// repository URL so credentials are not leaked when the URL is logged or exported (e.g. as a
|
||||
// tracing span attribute). If the URL cannot be parsed it is returned unchanged.
|
||||
func SanitizeRepoURL(repoURL string) string {
|
||||
u, err := url.Parse(repoURL)
|
||||
if err != nil || u.User == nil {
|
||||
return repoURL
|
||||
}
|
||||
u.User = nil
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func SameURL(leftRepo, rightRepo string) bool {
|
||||
normalLeft := NormalizeGitURLAllowInvalid(leftRepo)
|
||||
normalRight := NormalizeGitURLAllowInvalid(rightRepo)
|
||||
|
||||
@@ -145,6 +145,28 @@ func TestSameURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeRepoURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"no credentials", "https://github.com/argoproj/argo-cd.git", "https://github.com/argoproj/argo-cd.git"},
|
||||
{"strips user and password", "https://user:p@[email protected]/argoproj/argo-cd.git", "https://github.com/argoproj/argo-cd.git"},
|
||||
{"strips token-only userinfo", "https://[email protected]/org/repo", "https://github.com/org/repo"},
|
||||
{"scp-style ssh left unchanged", "[email protected]:argoproj/argo-cd.git", "[email protected]:argoproj/argo-cd.git"},
|
||||
{"unparseable returned unchanged", "://not a url", "://not a url"},
|
||||
{"empty returned unchanged", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Equal(t, tt.want, SanitizeRepoURL(tt.in))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomHTTPClient(t *testing.T) {
|
||||
proxy.UseTestingProxyCallback()
|
||||
|
||||
|
||||
+30
-2
@@ -8,16 +8,31 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.6.1"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// InitTracer initializes the trace provider and the otel grpc exporter.
|
||||
func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpInsecure bool, otlpHeaders map[string]string, otlpAttrs []string) (func(), error) {
|
||||
//
|
||||
// sampleRatio controls head-based sampling: 1.0 samples every trace, 0.0 samples
|
||||
// none, and values in between sample that fraction of traces. Callers are expected to
|
||||
// pass a value in [0.0, 1.0] (the --otlp-sample-ratio flag is range-validated at parse
|
||||
// time via cli.BoundedFloat64Var). The sampler is parent-based, so a sampling decision
|
||||
// made upstream (e.g. the controller) is honored by every downstream service the trace
|
||||
// context propagates to (repo-server, commit-server, ...), keeping each trace whole
|
||||
// rather than partially sampled across process boundaries.
|
||||
//
|
||||
// Because the sampler is parent-based, an incoming request that already carries a
|
||||
// W3C traceparent marked "not sampled" is not recorded even when sampleRatio is 1.0:
|
||||
// the upstream sampling decision wins. This differs from the previous always-on
|
||||
// sampler, which recorded every request regardless of any inbound sampling flag.
|
||||
func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpInsecure bool, otlpHeaders map[string]string, otlpAttrs []string, sampleRatio float64) (func(), error) {
|
||||
attrs := make([]attribute.KeyValue, 0, len(otlpAttrs))
|
||||
for i := range otlpAttrs {
|
||||
attr := otlpAttrs[i]
|
||||
@@ -61,7 +76,7 @@ func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpInsecu
|
||||
// span processor to aggregate spans before export.
|
||||
bsp := sdktrace.NewBatchSpanProcessor(exporter)
|
||||
provider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||||
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(sampleRatio))),
|
||||
sdktrace.WithResource(res),
|
||||
sdktrace.WithSpanProcessor(bsp),
|
||||
)
|
||||
@@ -76,3 +91,16 @@ func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpInsecu
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EndSpan ends span, recording an error status when err is non-nil. Defer it inside a
|
||||
// closure so err is read at function exit rather than at defer-statement time (when a
|
||||
// named return is still nil):
|
||||
//
|
||||
// defer func() { trace.EndSpan(span, retErr) }()
|
||||
func EndSpan(span oteltrace.Span, err error) {
|
||||
if err != nil {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user