mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-22 10:38:51 +01:00
Compare commits
3 Commits
source-hyd
...
hydrator-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd35d3dc23 | ||
|
|
fed8833669 | ||
|
|
1e71863944 |
8
.github/workflows/ci-build.yaml
vendored
8
.github/workflows/ci-build.yaml
vendored
@@ -370,11 +370,11 @@ jobs:
|
||||
path: test-results
|
||||
- name: combine-go-coverage
|
||||
# We generate coverage reports for all Argo CD components, but only the applicationset-controller,
|
||||
# app-controller, and repo-server report contain coverage data. The other components currently don't shut down
|
||||
# gracefully, so no coverage data is produced. Once those components are fixed, we can add references to their
|
||||
# coverage output directories.
|
||||
# app-controller, repo-server, and commit-server report contain coverage data. The other components currently
|
||||
# don't shut down gracefully, so no coverage data is produced. Once those components are fixed, we can add
|
||||
# references to their coverage output directories.
|
||||
run: |
|
||||
go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller -o test-results/full-coverage.out
|
||||
go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller,e2e-code-coverage/commit-server -o test-results/full-coverage.out
|
||||
- name: Upload code coverage information to codecov.io
|
||||
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
|
||||
with:
|
||||
|
||||
@@ -26,6 +26,13 @@ packages:
|
||||
github.com/argoproj/argo-cd/v2/applicationset/utils:
|
||||
interfaces:
|
||||
Renderer:
|
||||
github.com/argoproj/argo-cd/v2/commitserver/commit:
|
||||
interfaces:
|
||||
RepoClientFactory:
|
||||
github.com/argoproj/argo-cd/v2/commitserver/apiclient:
|
||||
interfaces:
|
||||
CommitServiceClient:
|
||||
Clientset:
|
||||
github.com/argoproj/argo-cd/v2/controller/cache:
|
||||
interfaces:
|
||||
LiveStateCache:
|
||||
|
||||
@@ -140,7 +140,8 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex && \
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications && \
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-applicationset-controller && \
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-k8s-auth
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-k8s-auth && \
|
||||
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server
|
||||
|
||||
USER $ARGOCD_USER_ID
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
|
||||
1
Makefile
1
Makefile
@@ -472,6 +472,7 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
|
||||
mkdir -p /tmp/coverage/repo-server
|
||||
mkdir -p /tmp/coverage/applicationset-controller
|
||||
mkdir -p /tmp/coverage/notification
|
||||
mkdir -p /tmp/coverage/commit-server
|
||||
# set paths for locally managed ssh known hosts and tls certs data
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
|
||||
3
Procfile
3
Procfile
@@ -1,9 +1,10 @@
|
||||
controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/app-controller} HOSTNAME=testappcontroller-1 FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --server-side-diff-enabled=${ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF:-'false'}"
|
||||
controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/app-controller} HOSTNAME=testappcontroller-1 FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --commit-server localhost:${ARGOCD_E2E_COMMITSERVER_PORT:-8086} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --server-side-diff-enabled=${ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF:-'false'} --hydrator-enabled=${ARGOCD_APPLICATION_CONTROLLER_HYDRATOR_ENABLED:='false'}"
|
||||
api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/api-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''}"
|
||||
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v2/cmd gendexcfg -o `pwd`/dist/dex.yaml && (test -f dist/dex.yaml || { echo 'Failed to generate dex configuration'; exit 1; }) && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:$(grep "image: ghcr.io/dexidp/dex" manifests/base/dex/argocd-dex-server-deployment.yaml | cut -d':' -f3) dex serve /dex.yaml"
|
||||
redis: hack/start-redis-with-password.sh
|
||||
repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/repo-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --otlp-address=${ARGOCD_OTLP_ADDRESS}"
|
||||
cmp-server: [ "$ARGOCD_E2E_TEST" = 'true' ] && exit 0 || [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-cmp-server ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} $COMMAND --config-dir-path ./test/cmp --loglevel debug --otlp-address=${ARGOCD_OTLP_ADDRESS}"
|
||||
commit-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/commit-server} FORCE_LOG_COLORS=1 ARGOCD_BINARY_NAME=argocd-commit-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_COMMITSERVER_PORT:-8086}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
helm-registry: test/fixture/testrepos/start-helm-registry.sh
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/bradleyfalzon/ghinstallation/v2"
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/google/go-github/v66/github"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/google/go-github/v66/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package pull_request
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/google/go-github/v66/github"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-github/v63/github"
|
||||
"github.com/google/go-github/v66/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
|
||||
commitclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
"github.com/argoproj/argo-cd/v2/controller"
|
||||
"github.com/argoproj/argo-cd/v2/controller/sharding"
|
||||
@@ -58,6 +59,7 @@ func NewCommand() *cobra.Command {
|
||||
repoErrorGracePeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
commitServerAddress string
|
||||
selfHealTimeoutSeconds int
|
||||
selfHealBackoffTimeoutSeconds int
|
||||
selfHealBackoffFactor int
|
||||
@@ -87,7 +89,8 @@ func NewCommand() *cobra.Command {
|
||||
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
|
||||
|
||||
// argocd k8s event logging flag
|
||||
enableK8sEvent []string
|
||||
enableK8sEvent []string
|
||||
hydratorEnabled bool
|
||||
)
|
||||
command := cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -157,6 +160,8 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds, tlsConfig)
|
||||
|
||||
commitClientset := commitclient.NewCommitServerClientset(commitServerAddress)
|
||||
|
||||
cache, err := cacheSource()
|
||||
errors.CheckError(err)
|
||||
cache.Cache.SetClient(cacheutil.NewTwoLevelClient(cache.Cache.GetClient(), 10*time.Minute))
|
||||
@@ -183,6 +188,7 @@ func NewCommand() *cobra.Command {
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
commitClientset,
|
||||
cache,
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
@@ -205,6 +211,7 @@ func NewCommand() *cobra.Command {
|
||||
enableDynamicClusterDistribution,
|
||||
ignoreNormalizerOpts,
|
||||
enableK8sEvent,
|
||||
hydratorEnabled,
|
||||
)
|
||||
errors.CheckError(err)
|
||||
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer(), nil)
|
||||
@@ -247,6 +254,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().Int64Var(&repoErrorGracePeriod, "repo-error-grace-period-seconds", int64(env.ParseDurationFromEnv("ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS", defaultAppResyncPeriod*time.Second, 0, math.MaxInt64).Seconds()), "Grace period in seconds for ignoring consecutive errors while communicating with repo server.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", env.StringFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER", common.DefaultRepoServerAddr), "Repo server address.")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.")
|
||||
command.Flags().StringVar(&commitServerAddress, "commit-server", env.StringFromEnv("ARGOCD_APPLICATION_CONTROLLER_COMMIT_SERVER", common.DefaultCommitServerAddr), "Commit server address.")
|
||||
command.Flags().IntVar(&statusProcessors, "status-processors", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_STATUS_PROCESSORS", 20, 0, math.MaxInt32), "Number of application status processors")
|
||||
command.Flags().IntVar(&operationProcessors, "operation-processors", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_OPERATION_PROCESSORS", 10, 0, math.MaxInt32), "Number of application operation processors")
|
||||
command.Flags().StringVar(&cmdutil.LogFormat, "logformat", env.StringFromEnv("ARGOCD_APPLICATION_CONTROLLER_LOGFORMAT", "text"), "Set the logging format. One of: text|json")
|
||||
@@ -285,7 +293,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout-seconds", env.ParseDurationFromEnv("ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT", 0*time.Second, 0, math.MaxInt64), "Set ignore normalizer JQ execution timeout")
|
||||
// argocd k8s event logging flag
|
||||
command.Flags().StringSliceVar(&enableK8sEvent, "enable-k8s-event", env.StringsFromEnv("ARGOCD_ENABLE_K8S_EVENT", argo.DefaultEnableEventList(), ","), "Enable ArgoCD to use k8s event. For disabling all events, set the value as `none`. (e.g --enable-k8s-event=none), For enabling specific events, set the value as `event reason`. (e.g --enable-k8s-event=StatusRefreshed,ResourceCreated)")
|
||||
|
||||
command.Flags().BoolVar(&hydratorEnabled, "hydrator-enabled", env.ParseBoolFromEnv("ARGOCD_HYDRATOR_ENABLED", false), "Feature flag to enable Hydrator. Default (\"false\")")
|
||||
cacheSource = appstatecache.AddCacheFlagsToCmd(&command, cacheutil.Options{
|
||||
OnClientCreated: func(client *redis.Client) {
|
||||
redisClient = client
|
||||
|
||||
117
cmd/argocd-commit-server/commands/argocd_commit_server.go
Normal file
117
cmd/argocd-commit-server/commands/argocd_commit_server.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
|
||||
cmdutil "github.com/argoproj/argo-cd/v2/cmd/util"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
"github.com/argoproj/argo-cd/v2/util/askpass"
|
||||
"github.com/argoproj/argo-cd/v2/util/cli"
|
||||
"github.com/argoproj/argo-cd/v2/util/env"
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
"github.com/argoproj/argo-cd/v2/util/healthz"
|
||||
ioutil "github.com/argoproj/argo-cd/v2/util/io"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd-commit-server command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
listenHost string
|
||||
listenPort int
|
||||
metricsPort int
|
||||
metricsHost string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "argocd-commit-server",
|
||||
Short: "Run Argo CD Commit Server",
|
||||
Long: "Argo CD Commit Server is an internal service which commits and pushes hydrated manifests to git. This command runs Commit Server in the foreground.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
vers := common.GetVersion()
|
||||
vers.LogStartupInfo(
|
||||
"Argo CD Commit Server",
|
||||
map[string]any{
|
||||
"port": listenPort,
|
||||
},
|
||||
)
|
||||
|
||||
cli.SetLogFormat(cmdutil.LogFormat)
|
||||
cli.SetLogLevel(cmdutil.LogLevel)
|
||||
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf("%s:%d", metricsHost, metricsPort), nil)) }()
|
||||
|
||||
askPassServer := askpass.NewServer(askpass.CommitServerSocketPath)
|
||||
go func() { errors.CheckError(askPassServer.Run()) }()
|
||||
|
||||
server := commitserver.NewServer(askPassServer, metricsServer)
|
||||
grpc := server.CreateGRPC()
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", listenHost, listenPort))
|
||||
errors.CheckError(err)
|
||||
|
||||
healthz.ServeHealthCheck(http.DefaultServeMux, func(r *http.Request) error {
|
||||
if val, ok := r.URL.Query()["full"]; ok && len(val) > 0 && val[0] == "true" {
|
||||
// connect to itself to make sure commit server is able to serve connection
|
||||
// used by liveness probe to auto restart commit server
|
||||
conn, err := apiclient.NewConnection(fmt.Sprintf("localhost:%d", listenPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.Close(conn)
|
||||
client := grpc_health_v1.NewHealthClient(conn)
|
||||
res, err := client.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
return fmt.Errorf("grpc health check status is '%v'", res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Graceful shutdown code adapted from here: https://gist.github.com/embano1/e0bf49d24f1cdd07cffad93097c04f0a
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
s := <-sigCh
|
||||
log.Printf("got signal %v, attempting graceful shutdown", s)
|
||||
grpc.GracefulStop()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
log.Println("starting grpc server")
|
||||
err = grpc.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
wg.Wait()
|
||||
log.Println("clean shutdown")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&cmdutil.LogFormat, "logformat", env.StringFromEnv("ARGOCD_COMMIT_SERVER_LOGFORMAT", "text"), "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&cmdutil.LogLevel, "loglevel", env.StringFromEnv("ARGOCD_COMMIT_SERVER_LOGLEVEL", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().StringVar(&listenHost, "address", env.StringFromEnv("ARGOCD_COMMIT_SERVER_LISTEN_ADDRESS", common.DefaultAddressCommitServer), "Listen on given address for incoming connections")
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortCommitServer, "Listen on given port for incoming connections")
|
||||
command.Flags().StringVar(&metricsHost, "metrics-address", env.StringFromEnv("ARGOCD_COMMIT_SERVER_METRICS_LISTEN_ADDRESS", common.DefaultAddressCommitServerMetrics), "Listen on given address for metrics")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortCommitServerMetrics, "Start metrics server on given port")
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
|
||||
"github.com/argoproj/argo-cd/v2/util/askpass"
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
grpc_util "github.com/argoproj/argo-cd/v2/util/grpc"
|
||||
"github.com/argoproj/argo-cd/v2/util/io"
|
||||
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
|
||||
reposervercache "github.com/argoproj/argo-cd/v2/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/v2/util/askpass"
|
||||
cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
|
||||
"github.com/argoproj/argo-cd/v2/util/cli"
|
||||
"github.com/argoproj/argo-cd/v2/util/env"
|
||||
|
||||
@@ -112,6 +112,7 @@ type watchOpts struct {
|
||||
suspended bool
|
||||
degraded bool
|
||||
delete bool
|
||||
hydrated bool
|
||||
}
|
||||
|
||||
// NewApplicationCreateCommand returns a new instance of an `argocd app create` command
|
||||
@@ -1883,6 +1884,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().BoolVar(&watch.suspended, "suspended", false, "Wait for suspended")
|
||||
command.Flags().BoolVar(&watch.degraded, "degraded", false, "Wait for degraded")
|
||||
command.Flags().BoolVar(&watch.delete, "delete", false, "Wait for delete")
|
||||
command.Flags().BoolVar(&watch.hydrated, "hydrated", false, "Wait for hydration operations")
|
||||
command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.")
|
||||
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator))
|
||||
command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations")
|
||||
@@ -2450,7 +2452,7 @@ func groupResourceStates(app *argoappv1.Application, selectedResources []*argoap
|
||||
}
|
||||
|
||||
// check if resource health, sync and operation statuses matches watch options
|
||||
func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation) bool {
|
||||
func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation, hydrationFinished bool) bool {
|
||||
if watch.delete {
|
||||
return false
|
||||
}
|
||||
@@ -2480,7 +2482,8 @@ func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string
|
||||
|
||||
synced := !watch.sync || syncStatus == string(argoappv1.SyncStatusCodeSynced)
|
||||
operational := !watch.operation || operationStatus == nil
|
||||
return synced && healthCheckPassed && operational
|
||||
hydration := !watch.hydrated || hydrationFinished
|
||||
return synced && healthCheckPassed && operational && hydration
|
||||
}
|
||||
|
||||
// resourceParentChild gets the latest state of the app and the latest state of the app's resource tree and then
|
||||
@@ -2644,13 +2647,15 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
|
||||
}
|
||||
}
|
||||
|
||||
hydrationFinished := app.Status.SourceHydrator.CurrentOperation != nil && app.Status.SourceHydrator.CurrentOperation.Phase == argoappv1.HydrateOperationPhaseHydrated && app.Status.SourceHydrator.CurrentOperation.SourceHydrator.DeepEquals(app.Status.SourceHydrator.LastSuccessfulOperation.SourceHydrator) && app.Status.SourceHydrator.CurrentOperation.DrySHA == app.Status.SourceHydrator.LastSuccessfulOperation.DrySHA
|
||||
|
||||
var selectedResourcesAreReady bool
|
||||
|
||||
// If selected resources are included, wait only on those resources, otherwise wait on the application as a whole.
|
||||
if len(selectedResources) > 0 {
|
||||
selectedResourcesAreReady = true
|
||||
for _, state := range getResourceStates(app, selectedResources) {
|
||||
resourceIsReady := checkResourceStatus(watch, state.Health, state.Status, appEvent.Application.Operation)
|
||||
resourceIsReady := checkResourceStatus(watch, state.Health, state.Status, appEvent.Application.Operation, hydrationFinished)
|
||||
if !resourceIsReady {
|
||||
selectedResourcesAreReady = false
|
||||
break
|
||||
@@ -2658,7 +2663,7 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client,
|
||||
}
|
||||
} else {
|
||||
// Wait on the application as a whole
|
||||
selectedResourcesAreReady = checkResourceStatus(watch, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation)
|
||||
selectedResourcesAreReady = checkResourceStatus(watch, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation, hydrationFinished)
|
||||
}
|
||||
|
||||
if selectedResourcesAreReady && (!operationInProgress || !watch.operation) {
|
||||
|
||||
@@ -1705,7 +1705,7 @@ func TestCheckResourceStatus(t *testing.T) {
|
||||
suspended: true,
|
||||
health: true,
|
||||
degraded: true,
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Degraded, Suspended and health status failed", func(t *testing.T) {
|
||||
@@ -1713,57 +1713,57 @@ func TestCheckResourceStatus(t *testing.T) {
|
||||
suspended: true,
|
||||
health: true,
|
||||
degraded: true,
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.False(t, res)
|
||||
})
|
||||
t.Run("Suspended and health status passed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: true,
|
||||
health: true,
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Suspended and health status failed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: true,
|
||||
health: true,
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.False(t, res)
|
||||
})
|
||||
t.Run("Suspended passed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: true,
|
||||
health: false,
|
||||
}, string(health.HealthStatusSuspended), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusSuspended), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Suspended failed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: true,
|
||||
health: false,
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.False(t, res)
|
||||
})
|
||||
t.Run("Health passed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: false,
|
||||
health: true,
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusHealthy), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Health failed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{
|
||||
suspended: false,
|
||||
health: true,
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.False(t, res)
|
||||
})
|
||||
t.Run("Synced passed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Synced failed", func(t *testing.T) {
|
||||
res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeOutOfSync), &v1alpha1.Operation{})
|
||||
res := checkResourceStatus(watchOpts{}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeOutOfSync), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Degraded passed", func(t *testing.T) {
|
||||
@@ -1771,7 +1771,7 @@ func TestCheckResourceStatus(t *testing.T) {
|
||||
suspended: false,
|
||||
health: false,
|
||||
degraded: true,
|
||||
}, string(health.HealthStatusDegraded), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusDegraded), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.True(t, res)
|
||||
})
|
||||
t.Run("Degraded failed", func(t *testing.T) {
|
||||
@@ -1779,7 +1779,7 @@ func TestCheckResourceStatus(t *testing.T) {
|
||||
suspended: false,
|
||||
health: false,
|
||||
degraded: true,
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{})
|
||||
}, string(health.HealthStatusProgressing), string(v1alpha1.SyncStatusCodeSynced), &v1alpha1.Operation{}, true)
|
||||
assert.False(t, res)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
appcontroller "github.com/argoproj/argo-cd/v2/cmd/argocd-application-controller/commands"
|
||||
applicationset "github.com/argoproj/argo-cd/v2/cmd/argocd-applicationset-controller/commands"
|
||||
cmpserver "github.com/argoproj/argo-cd/v2/cmd/argocd-cmp-server/commands"
|
||||
commitserver "github.com/argoproj/argo-cd/v2/cmd/argocd-commit-server/commands"
|
||||
dex "github.com/argoproj/argo-cd/v2/cmd/argocd-dex/commands"
|
||||
gitaskpass "github.com/argoproj/argo-cd/v2/cmd/argocd-git-ask-pass/commands"
|
||||
k8sauth "github.com/argoproj/argo-cd/v2/cmd/argocd-k8s-auth/commands"
|
||||
@@ -46,6 +47,8 @@ func main() {
|
||||
case "argocd-cmp-server":
|
||||
command = cmpserver.NewCommand()
|
||||
isCLI = true
|
||||
case "argocd-commit-server":
|
||||
command = commitserver.NewCommand()
|
||||
case "argocd-dex":
|
||||
command = dex.NewCommand()
|
||||
case "argocd-notifications":
|
||||
|
||||
@@ -91,6 +91,12 @@ type AppOptions struct {
|
||||
retryBackoffFactor int64
|
||||
ref string
|
||||
SourceName string
|
||||
drySourceRepo string
|
||||
drySourceRevision string
|
||||
drySourcePath string
|
||||
syncSourceBranch string
|
||||
syncSourcePath string
|
||||
hydrateToBranch string
|
||||
}
|
||||
|
||||
// SetAutoMaxProcs sets the GOMAXPROCS value based on the binary name.
|
||||
@@ -112,6 +118,12 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
|
||||
command.Flags().StringVar(&opts.chart, "helm-chart", "", "Helm Chart name")
|
||||
command.Flags().StringVar(&opts.env, "env", "", "Application environment to monitor")
|
||||
command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, commit or Helm chart version the application will sync to")
|
||||
command.Flags().StringVar(&opts.drySourceRepo, "dry-source-repo", "", "Repository URL of the app dry source")
|
||||
command.Flags().StringVar(&opts.drySourceRevision, "dry-source-revision", "", "Revision of the app dry source")
|
||||
command.Flags().StringVar(&opts.drySourcePath, "dry-source-path", "", "Path in repository to the app directory for the dry source")
|
||||
command.Flags().StringVar(&opts.syncSourceBranch, "sync-source-branch", "", "The branch from which the app will sync")
|
||||
command.Flags().StringVar(&opts.syncSourcePath, "sync-source-path", "", "The path in the repository from which the app will sync")
|
||||
command.Flags().StringVar(&opts.hydrateToBranch, "hydrate-to-branch", "", "The branch to hydrate the app to")
|
||||
command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", argoappv1.RevisionHistoryLimit, "How many items to keep in revision history")
|
||||
command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (e.g. https://kubernetes.default.svc)")
|
||||
command.Flags().StringVar(&opts.destName, "dest-name", "", "K8s cluster Name (e.g. minikube)")
|
||||
@@ -175,21 +187,27 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
if flags == nil {
|
||||
return visited
|
||||
}
|
||||
source := spec.GetSourcePtrByPosition(sourcePosition)
|
||||
if source == nil {
|
||||
source = &argoappv1.ApplicationSource{}
|
||||
}
|
||||
source, visited = ConstructSource(source, *appOpts, flags)
|
||||
if spec.HasMultipleSources() {
|
||||
if sourcePosition == 0 {
|
||||
spec.Sources[sourcePosition] = *source
|
||||
} else if sourcePosition > 0 {
|
||||
spec.Sources[sourcePosition-1] = *source
|
||||
} else {
|
||||
spec.Sources = append(spec.Sources, *source)
|
||||
}
|
||||
var h *argoappv1.SourceHydrator
|
||||
h, hasHydratorFlag := constructSourceHydrator(spec.SourceHydrator, *appOpts, flags)
|
||||
if hasHydratorFlag {
|
||||
spec.SourceHydrator = h
|
||||
} else {
|
||||
spec.Source = source
|
||||
source := spec.GetSourcePtrByPosition(sourcePosition)
|
||||
if source == nil {
|
||||
source = &argoappv1.ApplicationSource{}
|
||||
}
|
||||
source, visited = ConstructSource(source, *appOpts, flags)
|
||||
if spec.HasMultipleSources() {
|
||||
if sourcePosition == 0 {
|
||||
spec.Sources[sourcePosition] = *source
|
||||
} else if sourcePosition > 0 {
|
||||
spec.Sources[sourcePosition-1] = *source
|
||||
} else {
|
||||
spec.Sources = append(spec.Sources, *source)
|
||||
}
|
||||
} else {
|
||||
spec.Source = source
|
||||
}
|
||||
}
|
||||
flags.Visit(func(f *pflag.Flag) {
|
||||
visited++
|
||||
@@ -592,9 +610,7 @@ func constructAppsBaseOnName(appName string, labels, annotations, args []string,
|
||||
Name: appName,
|
||||
Namespace: appNs,
|
||||
},
|
||||
Spec: argoappv1.ApplicationSpec{
|
||||
Source: &argoappv1.ApplicationSource{},
|
||||
},
|
||||
Spec: argoappv1.ApplicationSpec{},
|
||||
}
|
||||
SetAppSpecOptions(flags, &app.Spec, &appOpts, 0)
|
||||
SetParameterOverrides(app, appOpts.Parameters, 0)
|
||||
@@ -768,6 +784,47 @@ func ConstructSource(source *argoappv1.ApplicationSource, appOpts AppOptions, fl
|
||||
return source, visited
|
||||
}
|
||||
|
||||
// constructSourceHydrator constructs a source hydrator from the command line flags. It returns the modified source
|
||||
// hydrator and a boolean indicating if any hydrator flags were set. We return instead of just modifying the source
|
||||
// hydrator in place because the given hydrator `h` might be nil. In that case, we need to create a new source hydrator
|
||||
// and return it.
|
||||
func constructSourceHydrator(h *argoappv1.SourceHydrator, appOpts AppOptions, flags *pflag.FlagSet) (*argoappv1.SourceHydrator, bool) {
|
||||
hasHydratorFlag := false
|
||||
ensureNotNil := func(notEmpty bool) {
|
||||
hasHydratorFlag = true
|
||||
if notEmpty && h == nil {
|
||||
h = &argoappv1.SourceHydrator{}
|
||||
}
|
||||
}
|
||||
flags.Visit(func(f *pflag.Flag) {
|
||||
switch f.Name {
|
||||
case "dry-source-repo":
|
||||
ensureNotNil(appOpts.drySourceRepo != "")
|
||||
h.DrySource.RepoURL = appOpts.drySourceRepo
|
||||
case "dry-source-path":
|
||||
ensureNotNil(appOpts.drySourcePath != "")
|
||||
h.DrySource.Path = appOpts.drySourcePath
|
||||
case "dry-source-revision":
|
||||
ensureNotNil(appOpts.drySourceRevision != "")
|
||||
h.DrySource.TargetRevision = appOpts.drySourceRevision
|
||||
case "sync-source-branch":
|
||||
ensureNotNil(appOpts.syncSourceBranch != "")
|
||||
h.SyncSource.TargetBranch = appOpts.syncSourceBranch
|
||||
case "sync-source-path":
|
||||
ensureNotNil(appOpts.syncSourcePath != "")
|
||||
h.SyncSource.Path = appOpts.syncSourcePath
|
||||
case "hydrate-to-branch":
|
||||
ensureNotNil(appOpts.hydrateToBranch != "")
|
||||
if appOpts.hydrateToBranch == "" {
|
||||
h.HydrateTo = nil
|
||||
} else {
|
||||
h.HydrateTo = &argoappv1.HydrateTo{TargetBranch: appOpts.hydrateToBranch}
|
||||
}
|
||||
}
|
||||
})
|
||||
return h, hasHydratorFlag
|
||||
}
|
||||
|
||||
func mergeLabels(app *argoappv1.Application, labels []string) {
|
||||
mapLabels, err := label.Parse(labels)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -295,6 +295,28 @@ func Test_setAppSpecOptions(t *testing.T) {
|
||||
require.NoError(t, f.SetFlag("helm-api-versions", "v2"))
|
||||
assert.Equal(t, []string{"v1", "v2"}, f.spec.Source.Helm.APIVersions)
|
||||
})
|
||||
t.Run("source hydrator", func(t *testing.T) {
|
||||
require.NoError(t, f.SetFlag("dry-source-repo", "https://github.com/argoproj/argocd-example-apps"))
|
||||
assert.Equal(t, "https://github.com/argoproj/argocd-example-apps", f.spec.SourceHydrator.DrySource.RepoURL)
|
||||
|
||||
require.NoError(t, f.SetFlag("dry-source-path", "apps"))
|
||||
assert.Equal(t, "apps", f.spec.SourceHydrator.DrySource.Path)
|
||||
|
||||
require.NoError(t, f.SetFlag("dry-source-revision", "HEAD"))
|
||||
assert.Equal(t, "HEAD", f.spec.SourceHydrator.DrySource.TargetRevision)
|
||||
|
||||
require.NoError(t, f.SetFlag("sync-source-branch", "env/test"))
|
||||
assert.Equal(t, "env/test", f.spec.SourceHydrator.SyncSource.TargetBranch)
|
||||
|
||||
require.NoError(t, f.SetFlag("sync-source-path", "apps"))
|
||||
assert.Equal(t, "apps", f.spec.SourceHydrator.SyncSource.Path)
|
||||
|
||||
require.NoError(t, f.SetFlag("hydrate-to-branch", "env/test-next"))
|
||||
assert.Equal(t, "env/test-next", f.spec.SourceHydrator.HydrateTo.TargetBranch)
|
||||
|
||||
require.NoError(t, f.SetFlag("hydrate-to-branch", ""))
|
||||
assert.Nil(t, f.spec.SourceHydrator.HydrateTo)
|
||||
})
|
||||
}
|
||||
|
||||
func newMultiSourceAppOptionsFixture() *appOptionsFixture {
|
||||
|
||||
49
commitserver/apiclient/clientset.go
Normal file
49
commitserver/apiclient/clientset.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package apiclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/io"
|
||||
)
|
||||
|
||||
// Clientset represents commit server api clients
|
||||
type Clientset interface {
|
||||
NewCommitServerClient() (io.Closer, CommitServiceClient, error)
|
||||
}
|
||||
|
||||
type clientSet struct {
|
||||
address string
|
||||
}
|
||||
|
||||
// NewCommitServerClient creates new instance of commit server client
|
||||
func (c *clientSet) NewCommitServerClient() (io.Closer, CommitServiceClient, error) {
|
||||
conn, err := NewConnection(c.address)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open a new connection to commit server: %w", err)
|
||||
}
|
||||
return conn, NewCommitServiceClient(conn), nil
|
||||
}
|
||||
|
||||
// NewConnection creates new connection to commit server
|
||||
func NewConnection(address string) (*grpc.ClientConn, error) {
|
||||
var opts []grpc.DialOption
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// TODO: switch to grpc.NewClient.
|
||||
// nolint:staticcheck
|
||||
conn, err := grpc.Dial(address, opts...)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to connect to commit service with address %s", address)
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// NewCommitServerClientset creates new instance of commit server Clientset
|
||||
func NewCommitServerClientset(address string) Clientset {
|
||||
return &clientSet{address: address}
|
||||
}
|
||||
1382
commitserver/apiclient/commit.pb.go
generated
Normal file
1382
commitserver/apiclient/commit.pb.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
68
commitserver/apiclient/mocks/Clientset.go
generated
Normal file
68
commitserver/apiclient/mocks/Clientset.go
generated
Normal file
@@ -0,0 +1,68 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
apiclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
io "github.com/argoproj/argo-cd/v2/util/io"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Clientset is an autogenerated mock type for the Clientset type
|
||||
type Clientset struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// NewCommitServerClient provides a mock function with given fields:
|
||||
func (_m *Clientset) NewCommitServerClient() (io.Closer, apiclient.CommitServiceClient, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NewCommitServerClient")
|
||||
}
|
||||
|
||||
var r0 io.Closer
|
||||
var r1 apiclient.CommitServiceClient
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func() (io.Closer, apiclient.CommitServiceClient, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() io.Closer); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.Closer)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() apiclient.CommitServiceClient); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(apiclient.CommitServiceClient)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func() error); ok {
|
||||
r2 = rf()
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// NewClientset creates a new instance of Clientset. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewClientset(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Clientset {
|
||||
mock := &Clientset{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
69
commitserver/apiclient/mocks/CommitServiceClient.go
generated
Normal file
69
commitserver/apiclient/mocks/CommitServiceClient.go
generated
Normal file
@@ -0,0 +1,69 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
apiclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CommitServiceClient is an autogenerated mock type for the CommitServiceClient type
|
||||
type CommitServiceClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CommitHydratedManifests provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *CommitServiceClient) CommitHydratedManifests(ctx context.Context, in *apiclient.CommitHydratedManifestsRequest, opts ...grpc.CallOption) (*apiclient.CommitHydratedManifestsResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CommitHydratedManifests")
|
||||
}
|
||||
|
||||
var r0 *apiclient.CommitHydratedManifestsResponse
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *apiclient.CommitHydratedManifestsRequest, ...grpc.CallOption) (*apiclient.CommitHydratedManifestsResponse, error)); ok {
|
||||
return rf(ctx, in, opts...)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *apiclient.CommitHydratedManifestsRequest, ...grpc.CallOption) *apiclient.CommitHydratedManifestsResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*apiclient.CommitHydratedManifestsResponse)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *apiclient.CommitHydratedManifestsRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewCommitServiceClient creates a new instance of CommitServiceClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewCommitServiceClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *CommitServiceClient {
|
||||
mock := &CommitServiceClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
225
commitserver/commit/commit.go
Normal file
225
commitserver/commit/commit.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
"github.com/argoproj/argo-cd/v2/util/io/files"
|
||||
)
|
||||
|
||||
// Service is the service that handles commit requests.
|
||||
type Service struct {
|
||||
gitCredsStore git.CredsStore
|
||||
metricsServer *metrics.Server
|
||||
repoClientFactory RepoClientFactory
|
||||
}
|
||||
|
||||
// NewService returns a new instance of the commit service.
|
||||
func NewService(gitCredsStore git.CredsStore, metricsServer *metrics.Server) *Service {
|
||||
return &Service{
|
||||
gitCredsStore: gitCredsStore,
|
||||
metricsServer: metricsServer,
|
||||
repoClientFactory: NewRepoClientFactory(gitCredsStore, metricsServer),
|
||||
}
|
||||
}
|
||||
|
||||
// CommitHydratedManifests handles a commit request. It clones the repository, checks out the sync branch, checks out
|
||||
// the target branch, clears the repository contents, writes the manifests to the repository, commits the changes, and
|
||||
// pushes the changes. It returns the hydrated revision SHA and an error if one occurred.
|
||||
func (s *Service) CommitHydratedManifests(ctx context.Context, r *apiclient.CommitHydratedManifestsRequest) (*apiclient.CommitHydratedManifestsResponse, error) {
|
||||
// This method is intentionally short. It's a wrapper around handleCommitRequest that adds metrics and logging.
|
||||
// Keep logic here minimal and put most of the logic in handleCommitRequest.
|
||||
startTime := time.Now()
|
||||
|
||||
// We validate for a nil repo in handleCommitRequest, but we need to check for a nil repo here to get the repo URL
|
||||
// for metrics.
|
||||
var repoURL string
|
||||
if r.Repo != nil {
|
||||
repoURL = r.Repo.Repo
|
||||
}
|
||||
|
||||
var err error
|
||||
s.metricsServer.IncPendingCommitRequest(repoURL)
|
||||
defer func() {
|
||||
s.metricsServer.DecPendingCommitRequest(repoURL)
|
||||
commitResponseType := metrics.CommitResponseTypeSuccess
|
||||
if err != nil {
|
||||
commitResponseType = metrics.CommitResponseTypeFailure
|
||||
}
|
||||
s.metricsServer.IncCommitRequest(repoURL, commitResponseType)
|
||||
s.metricsServer.ObserveCommitRequestDuration(repoURL, commitResponseType, time.Since(startTime))
|
||||
}()
|
||||
|
||||
logCtx := log.WithFields(log.Fields{"branch": r.TargetBranch, "drySHA": r.DrySha})
|
||||
|
||||
out, sha, err := s.handleCommitRequest(logCtx, r)
|
||||
if err != nil {
|
||||
logCtx.WithError(err).WithField("output", out).Error("failed to handle commit request")
|
||||
|
||||
// No need to wrap this error, sufficient context is build in handleCommitRequest.
|
||||
return &apiclient.CommitHydratedManifestsResponse{}, err
|
||||
}
|
||||
|
||||
logCtx.Info("Successfully handled commit request")
|
||||
return &apiclient.CommitHydratedManifestsResponse{
|
||||
HydratedSha: sha,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// handleCommitRequest handles the commit request. It clones the repository, checks out the sync branch, checks out the
|
||||
// target branch, clears the repository contents, writes the manifests to the repository, commits the changes, and pushes
|
||||
// the changes. It returns the output of the git commands and an error if one occurred.
|
||||
func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydratedManifestsRequest) (string, string, error) {
|
||||
if r.Repo == nil {
|
||||
return "", "", fmt.Errorf("repo is required")
|
||||
}
|
||||
if r.Repo.Repo == "" {
|
||||
return "", "", fmt.Errorf("repo URL is required")
|
||||
}
|
||||
if r.TargetBranch == "" {
|
||||
return "", "", fmt.Errorf("target branch is required")
|
||||
}
|
||||
if r.SyncBranch == "" {
|
||||
return "", "", fmt.Errorf("sync branch is required")
|
||||
}
|
||||
|
||||
logCtx = logCtx.WithField("repo", r.Repo.Repo)
|
||||
logCtx.Debug("Initiating git client")
|
||||
gitClient, dirPath, cleanup, err := s.initGitClient(logCtx, r)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to init git client: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
logCtx.Debugf("Checking out sync branch %s", r.SyncBranch)
|
||||
var out string
|
||||
out, err = gitClient.CheckoutOrOrphan(r.SyncBranch, false)
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to checkout sync branch: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debugf("Checking out target branch %s", r.TargetBranch)
|
||||
out, err = gitClient.CheckoutOrNew(r.TargetBranch, r.SyncBranch, false)
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to checkout target branch: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debug("Clearing repo contents")
|
||||
out, err = gitClient.RemoveContents()
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to clear repo: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debug("Writing manifests")
|
||||
err = WriteForPaths(dirPath, r.Repo.Repo, r.DrySha, r.Paths)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to write manifests: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debug("Committing and pushing changes")
|
||||
out, err = gitClient.CommitAndPush(r.TargetBranch, r.CommitMessage)
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to commit and push: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debug("Getting commit SHA")
|
||||
sha, err := gitClient.CommitSHA()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get commit SHA: %w", err)
|
||||
}
|
||||
|
||||
return "", sha, nil
|
||||
}
|
||||
|
||||
// initGitClient initializes a git client for the given repository and returns the client, the path to the directory where
|
||||
// the repository is cloned, a cleanup function that should be called when the directory is no longer needed, and an error
|
||||
// if one occurred.
|
||||
func (s *Service) initGitClient(logCtx *log.Entry, r *apiclient.CommitHydratedManifestsRequest) (git.Client, string, func(), error) {
|
||||
dirPath, err := files.CreateTempDir("/tmp/_commit-service")
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
// Call cleanupOrLog in this function if an error occurs to ensure the temp dir is cleaned up.
|
||||
cleanupOrLog := func() {
|
||||
err := os.RemoveAll(dirPath)
|
||||
if err != nil {
|
||||
logCtx.WithError(err).Error("failed to cleanup temp dir")
|
||||
}
|
||||
}
|
||||
|
||||
gitClient, err := s.repoClientFactory.NewClient(r.Repo, dirPath)
|
||||
if err != nil {
|
||||
cleanupOrLog()
|
||||
return nil, "", nil, fmt.Errorf("failed to create git client: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debugf("Initializing repo %s", r.Repo.Repo)
|
||||
err = gitClient.Init()
|
||||
if err != nil {
|
||||
cleanupOrLog()
|
||||
return nil, "", nil, fmt.Errorf("failed to init git client: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debugf("Fetching repo %s", r.Repo.Repo)
|
||||
err = gitClient.Fetch("")
|
||||
if err != nil {
|
||||
cleanupOrLog()
|
||||
return nil, "", nil, fmt.Errorf("failed to clone repo: %w", err)
|
||||
}
|
||||
|
||||
// FIXME: make it work for GHE
|
||||
//logCtx.Debugf("Getting user info for repo credentials")
|
||||
//gitCreds := r.Repo.GetGitCreds(s.gitCredsStore)
|
||||
//startTime := time.Now()
|
||||
//authorName, authorEmail, err := gitCreds.GetUserInfo(ctx)
|
||||
//s.metricsServer.ObserveUserInfoRequestDuration(r.Repo.Repo, getCredentialType(r.Repo), time.Since(startTime))
|
||||
//if err != nil {
|
||||
// cleanupOrLog()
|
||||
// return nil, "", nil, fmt.Errorf("failed to get github app info: %w", err)
|
||||
//}
|
||||
var authorName, authorEmail string
|
||||
|
||||
if authorName == "" {
|
||||
authorName = "Argo CD"
|
||||
}
|
||||
if authorEmail == "" {
|
||||
logCtx.Warnf("Author email not available, using 'argo-cd@example.com'.")
|
||||
authorEmail = "argo-cd@example.com"
|
||||
}
|
||||
|
||||
logCtx.Debugf("Setting author %s <%s>", authorName, authorEmail)
|
||||
_, err = gitClient.SetAuthor(authorName, authorEmail)
|
||||
if err != nil {
|
||||
cleanupOrLog()
|
||||
return nil, "", nil, fmt.Errorf("failed to set author: %w", err)
|
||||
}
|
||||
|
||||
return gitClient, dirPath, cleanupOrLog, nil
|
||||
}
|
||||
|
||||
type hydratorMetadataFile struct {
|
||||
RepoURL string `json:"repoURL"`
|
||||
DrySHA string `json:"drySha"`
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
|
||||
// TODO: make this configurable via ConfigMap.
|
||||
var manifestHydrationReadmeTemplate = `
|
||||
# Manifest Hydration
|
||||
|
||||
To hydrate the manifests in this repository, run the following commands:
|
||||
|
||||
` + "```shell\n" + `
|
||||
git clone {{ .RepoURL }}
|
||||
# cd into the cloned directory
|
||||
git checkout {{ .DrySHA }}
|
||||
{{ range $command := .Commands -}}
|
||||
{{ $command }}
|
||||
{{ end -}}` + "```"
|
||||
50
commitserver/commit/commit.proto
Normal file
50
commitserver/commit/commit.proto
Normal file
@@ -0,0 +1,50 @@
|
||||
syntax = "proto3";
|
||||
option go_package = "github.com/argoproj/argo-cd/v2/commitserver/apiclient";
|
||||
|
||||
import "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1/generated.proto";
|
||||
|
||||
// CommitHydratedManifestsRequest is the request to commit hydrated manifests to a repository.
|
||||
message CommitHydratedManifestsRequest {
|
||||
// Repo contains repository information including, at minimum, the URL of the repository. Generally it will contain
|
||||
// repo credentials.
|
||||
github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repo = 1;
|
||||
// SyncBranch is the branch Argo CD syncs from, i.e. the hydrated branch.
|
||||
string syncBranch = 2;
|
||||
// TargetBranch is the branch Argo CD is committing to, i.e. the branch that will be updated.
|
||||
string targetBranch = 3;
|
||||
// DrySha is the commit SHA from the dry branch, i.e. pre-rendered manifest branch.
|
||||
string drySha = 4;
|
||||
// CommitMessage is the commit message to use when committing changes.
|
||||
string commitMessage = 5;
|
||||
// Paths contains the paths to write hydrated manifests to, along with the manifests and commands to execute.
|
||||
repeated PathDetails paths = 6;
|
||||
}
|
||||
|
||||
// PathDetails holds information about hydrated manifests to be written to a particular path in the hydrated manifests
|
||||
// commit.
|
||||
message PathDetails {
|
||||
// Path is the path to write the hydrated manifests to.
|
||||
string path = 1;
|
||||
// Manifests contains the manifests to write to the path.
|
||||
repeated HydratedManifestDetails manifests = 2;
|
||||
// Commands contains the commands executed when hydrating the manifests.
|
||||
repeated string commands = 3;
|
||||
}
|
||||
|
||||
// ManifestDetails contains the hydrated manifests.
|
||||
message HydratedManifestDetails {
|
||||
// ManifestJSON is the hydrated manifest as JSON.
|
||||
string manifestJSON = 1;
|
||||
}
|
||||
|
||||
// ManifestsResponse is the response to the ManifestsRequest.
|
||||
message CommitHydratedManifestsResponse {
|
||||
// HydratedSha is the commit SHA of the hydrated manifests commit.
|
||||
string hydratedSha = 1;
|
||||
}
|
||||
|
||||
// CommitService is the service for committing hydrated manifests to a repository.
|
||||
service CommitService {
|
||||
// Commit commits hydrated manifests to a repository.
|
||||
rpc CommitHydratedManifests (CommitHydratedManifestsRequest) returns (CommitHydratedManifestsResponse);
|
||||
}
|
||||
125
commitserver/commit/commit_test.go
Normal file
125
commitserver/commit/commit_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/commit/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks"
|
||||
)
|
||||
|
||||
func Test_CommitHydratedManifests(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
validRequest := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
SyncBranch: "env/test",
|
||||
CommitMessage: "test commit message",
|
||||
}
|
||||
|
||||
t.Run("missing repo", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, _ := newServiceWithMocks(t)
|
||||
request := &apiclient.CommitHydratedManifestsRequest{}
|
||||
_, err := service.CommitHydratedManifests(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "repo is required")
|
||||
})
|
||||
|
||||
t.Run("missing repo URL", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, _ := newServiceWithMocks(t)
|
||||
request := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{},
|
||||
}
|
||||
_, err := service.CommitHydratedManifests(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "repo URL is required")
|
||||
})
|
||||
|
||||
t.Run("missing target branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, _ := newServiceWithMocks(t)
|
||||
request := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
}
|
||||
_, err := service.CommitHydratedManifests(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "target branch is required")
|
||||
})
|
||||
|
||||
t.Run("missing sync branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, _ := newServiceWithMocks(t)
|
||||
request := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
}
|
||||
_, err := service.CommitHydratedManifests(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "sync branch is required")
|
||||
})
|
||||
|
||||
t.Run("failed to create git client", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(nil, assert.AnError).Once()
|
||||
|
||||
_, err := service.CommitHydratedManifests(context.Background(), validRequest)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
})
|
||||
|
||||
t.Run("happy path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockGitClient := gitmocks.NewClient(t)
|
||||
mockGitClient.On("Init").Return(nil).Once()
|
||||
mockGitClient.On("Fetch", mock.Anything).Return(nil).Once()
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("RemoveContents").Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("it-worked!", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
|
||||
resp, err := service.CommitHydratedManifests(context.Background(), validRequest)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "it-worked!", resp.HydratedSha)
|
||||
})
|
||||
}
|
||||
|
||||
func newServiceWithMocks(t *testing.T) (*Service, *mocks.RepoClientFactory) {
|
||||
t.Helper()
|
||||
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
mockCredsStore := git.NoopCredsStore{}
|
||||
service := NewService(mockCredsStore, metricsServer)
|
||||
mockRepoClientFactory := mocks.NewRepoClientFactory(t)
|
||||
service.repoClientFactory = mockRepoClientFactory
|
||||
|
||||
return service, mockRepoClientFactory
|
||||
}
|
||||
23
commitserver/commit/credentialtypehelper.go
Normal file
23
commitserver/commit/credentialtypehelper.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package commit
|
||||
|
||||
import "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
|
||||
// getCredentialType returns the type of credential used by the repository.
|
||||
func getCredentialType(repo *v1alpha1.Repository) string {
|
||||
if repo == nil {
|
||||
return ""
|
||||
}
|
||||
if repo.Password != "" {
|
||||
return "https"
|
||||
}
|
||||
if repo.SSHPrivateKey != "" {
|
||||
return "ssh"
|
||||
}
|
||||
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
|
||||
return "github-app"
|
||||
}
|
||||
if repo.GCPServiceAccountKey != "" {
|
||||
return "cloud-source-repositories"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
62
commitserver/commit/credentialtypehelper_test.go
Normal file
62
commitserver/commit/credentialtypehelper_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestRepository_GetCredentialType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
repo *v1alpha1.Repository
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty Repository",
|
||||
repo: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "HTTPS Repository",
|
||||
repo: &v1alpha1.Repository{
|
||||
Repo: "foo",
|
||||
Password: "some-password",
|
||||
},
|
||||
want: "https",
|
||||
},
|
||||
{
|
||||
name: "SSH Repository",
|
||||
repo: &v1alpha1.Repository{
|
||||
Repo: "foo",
|
||||
SSHPrivateKey: "some-key",
|
||||
},
|
||||
want: "ssh",
|
||||
},
|
||||
{
|
||||
name: "GitHub App Repository",
|
||||
repo: &v1alpha1.Repository{
|
||||
Repo: "foo",
|
||||
GithubAppPrivateKey: "some-key",
|
||||
GithubAppId: 1,
|
||||
GithubAppInstallationId: 1,
|
||||
},
|
||||
want: "github-app",
|
||||
},
|
||||
{
|
||||
name: "Google Cloud Repository",
|
||||
repo: &v1alpha1.Repository{
|
||||
Repo: "foo",
|
||||
GCPServiceAccountKey: "some-key",
|
||||
},
|
||||
want: "cloud-source-repositories",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getCredentialType(tt.repo); got != tt.want {
|
||||
t.Errorf("Repository.GetCredentialType() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
145
commitserver/commit/hydratorhelper.go
Normal file
145
commitserver/commit/hydratorhelper.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/util/io/files"
|
||||
)
|
||||
|
||||
// WriteForPaths writes the manifests, hydrator.metadata, and README.md files for each path in the provided paths. It
|
||||
// also writes a root-level hydrator.metadata file containing the repo URL and dry SHA.
|
||||
func WriteForPaths(rootPath string, repoUrl string, drySha string, paths []*apiclient.PathDetails) error {
|
||||
// Write the top-level readme.
|
||||
err := writeMetadata(rootPath, hydratorMetadataFile{DrySHA: drySha, RepoURL: repoUrl})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write top-level hydrator metadata: %w", err)
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
hydratePath := p.Path
|
||||
if hydratePath == "." {
|
||||
hydratePath = ""
|
||||
}
|
||||
|
||||
var fullHydratePath string
|
||||
fullHydratePath, err = files.SecureMkdirAll(rootPath, hydratePath, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path: %w", err)
|
||||
}
|
||||
|
||||
// Write the manifests
|
||||
err = writeManifests(fullHydratePath, p.Manifests)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write manifests: %w", err)
|
||||
}
|
||||
|
||||
// Write hydrator.metadata containing information about the hydration process.
|
||||
hydratorMetadata := hydratorMetadataFile{
|
||||
Commands: p.Commands,
|
||||
DrySHA: drySha,
|
||||
RepoURL: repoUrl,
|
||||
}
|
||||
err = writeMetadata(fullHydratePath, hydratorMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write hydrator metadata: %w", err)
|
||||
}
|
||||
|
||||
// Write README
|
||||
err = writeReadme(fullHydratePath, hydratorMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write readme: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeMetadata writes the metadata to the hydrator.metadata file.
|
||||
func writeMetadata(dirPath string, metadata hydratorMetadataFile) error {
|
||||
hydratorMetadataJson, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal hydrator metadata: %w", err)
|
||||
}
|
||||
// No need to use SecureJoin here, as the path is already sanitized.
|
||||
hydratorMetadataPath := path.Join(dirPath, "hydrator.metadata")
|
||||
err = os.WriteFile(hydratorMetadataPath, hydratorMetadataJson, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write hydrator metadata: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeReadme writes the readme to the README.md file.
|
||||
func writeReadme(dirPath string, metadata hydratorMetadataFile) error {
|
||||
readmeTemplate := template.New("readme")
|
||||
readmeTemplate, err := readmeTemplate.Parse(manifestHydrationReadmeTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse readme template: %w", err)
|
||||
}
|
||||
// Create writer to template into
|
||||
// No need to use SecureJoin here, as the path is already sanitized.
|
||||
readmePath := path.Join(dirPath, "README.md")
|
||||
readmeFile, err := os.Create(readmePath)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to create README file: %w", err)
|
||||
}
|
||||
err = readmeTemplate.Execute(readmeFile, metadata)
|
||||
closeErr := readmeFile.Close()
|
||||
if closeErr != nil {
|
||||
log.WithError(closeErr).Error("failed to close README file")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute readme template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeManifests writes the manifests to the manifest.yaml file, truncating the file if it exists and appending the
|
||||
// manifests in the order they are provided.
|
||||
func writeManifests(dirPath string, manifests []*apiclient.HydratedManifestDetails) error {
|
||||
// If the file exists, truncate it.
|
||||
// No need to use SecureJoin here, as the path is already sanitized.
|
||||
manifestPath := path.Join(dirPath, "manifest.yaml")
|
||||
|
||||
file, err := os.OpenFile(manifestPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open manifest file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to close file")
|
||||
}
|
||||
}()
|
||||
|
||||
enc := yaml.NewEncoder(file)
|
||||
defer func() {
|
||||
err := enc.Close()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("failed to close yaml encoder")
|
||||
}
|
||||
}()
|
||||
enc.SetIndent(2)
|
||||
|
||||
for _, m := range manifests {
|
||||
obj := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(m.ManifestJSON), obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal manifest: %w", err)
|
||||
}
|
||||
err = enc.Encode(&obj.Object)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode manifest: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
135
commitserver/commit/hydratorhelper_test.go
Normal file
135
commitserver/commit/hydratorhelper_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
)
|
||||
|
||||
func TestWriteForPaths(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
repoUrl := "https://github.com/example/repo"
|
||||
drySha := "abc123"
|
||||
paths := []*apiclient.PathDetails{
|
||||
{
|
||||
Path: "path1",
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
|
||||
},
|
||||
Commands: []string{"command1", "command2"},
|
||||
},
|
||||
{
|
||||
Path: "path2",
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{ManifestJSON: `{"kind":"Service","apiVersion":"v1"}`},
|
||||
},
|
||||
Commands: []string{"command3"},
|
||||
},
|
||||
}
|
||||
|
||||
err := WriteForPaths(dir, repoUrl, drySha, paths)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check if the top-level hydrator.metadata exists and contains the repo URL and dry SHA
|
||||
topMetadataPath := path.Join(dir, "hydrator.metadata")
|
||||
topMetadataBytes, err := os.ReadFile(topMetadataPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var topMetadata hydratorMetadataFile
|
||||
err = json.Unmarshal(topMetadataBytes, &topMetadata)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repoUrl, topMetadata.RepoURL)
|
||||
assert.Equal(t, drySha, topMetadata.DrySHA)
|
||||
|
||||
for _, p := range paths {
|
||||
fullHydratePath, err := securejoin.SecureJoin(dir, p.Path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check if each path directory exists
|
||||
assert.DirExists(t, fullHydratePath)
|
||||
|
||||
// Check if each path contains a hydrator.metadata file and contains the repo URL
|
||||
metadataPath := path.Join(fullHydratePath, "hydrator.metadata")
|
||||
metadataBytes, err := os.ReadFile(metadataPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var readMetadata hydratorMetadataFile
|
||||
err = json.Unmarshal(metadataBytes, &readMetadata)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, repoUrl, readMetadata.RepoURL)
|
||||
|
||||
// Check if each path contains a README.md file and contains the repo URL
|
||||
readmePath := path.Join(fullHydratePath, "README.md")
|
||||
readmeBytes, err := os.ReadFile(readmePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(readmeBytes), repoUrl)
|
||||
|
||||
// Check if each path contains a manifest.yaml file and contains the word Pod
|
||||
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
|
||||
manifestBytes, err := os.ReadFile(manifestPath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(manifestBytes), "kind")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteMetadata(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
metadata := hydratorMetadataFile{
|
||||
RepoURL: "https://github.com/example/repo",
|
||||
DrySHA: "abc123",
|
||||
}
|
||||
|
||||
err := writeMetadata(dir, metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadataPath := path.Join(dir, "hydrator.metadata")
|
||||
metadataBytes, err := os.ReadFile(metadataPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var readMetadata hydratorMetadataFile
|
||||
err = json.Unmarshal(metadataBytes, &readMetadata)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, metadata, readMetadata)
|
||||
}
|
||||
|
||||
func TestWriteReadme(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
metadata := hydratorMetadataFile{
|
||||
RepoURL: "https://github.com/example/repo",
|
||||
DrySHA: "abc123",
|
||||
}
|
||||
|
||||
err := writeReadme(dir, metadata)
|
||||
require.NoError(t, err)
|
||||
|
||||
readmePath := path.Join(dir, "README.md")
|
||||
readmeBytes, err := os.ReadFile(readmePath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(readmeBytes), metadata.RepoURL)
|
||||
}
|
||||
|
||||
func TestWriteManifests(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
manifests := []*apiclient.HydratedManifestDetails{
|
||||
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
|
||||
}
|
||||
|
||||
err := writeManifests(dir, manifests)
|
||||
require.NoError(t, err)
|
||||
|
||||
manifestPath := path.Join(dir, "manifest.yaml")
|
||||
manifestBytes, err := os.ReadFile(manifestPath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(manifestBytes), "kind")
|
||||
}
|
||||
59
commitserver/commit/mocks/RepoClientFactory.go
generated
Normal file
59
commitserver/commit/mocks/RepoClientFactory.go
generated
Normal file
@@ -0,0 +1,59 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
git "github.com/argoproj/argo-cd/v2/util/git"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// RepoClientFactory is an autogenerated mock type for the RepoClientFactory type
|
||||
type RepoClientFactory struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// NewClient provides a mock function with given fields: repo, rootPath
|
||||
func (_m *RepoClientFactory) NewClient(repo *v1alpha1.Repository, rootPath string) (git.Client, error) {
|
||||
ret := _m.Called(repo, rootPath)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NewClient")
|
||||
}
|
||||
|
||||
var r0 git.Client
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(*v1alpha1.Repository, string) (git.Client, error)); ok {
|
||||
return rf(repo, rootPath)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(*v1alpha1.Repository, string) git.Client); ok {
|
||||
r0 = rf(repo, rootPath)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(git.Client)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(*v1alpha1.Repository, string) error); ok {
|
||||
r1 = rf(repo, rootPath)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewRepoClientFactory creates a new instance of RepoClientFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewRepoClientFactory(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *RepoClientFactory {
|
||||
mock := &RepoClientFactory{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
32
commitserver/commit/repo_client_factory.go
Normal file
32
commitserver/commit/repo_client_factory.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
)
|
||||
|
||||
// RepoClientFactory is a factory for creating git clients for a repository.
|
||||
type RepoClientFactory interface {
|
||||
NewClient(repo *v1alpha1.Repository, rootPath string) (git.Client, error)
|
||||
}
|
||||
|
||||
type repoClientFactory struct {
|
||||
gitCredsStore git.CredsStore
|
||||
metricsServer *metrics.Server
|
||||
}
|
||||
|
||||
// NewRepoClientFactory returns a new instance of the repo client factory.
|
||||
func NewRepoClientFactory(gitCredsStore git.CredsStore, metricsServer *metrics.Server) RepoClientFactory {
|
||||
return &repoClientFactory{
|
||||
gitCredsStore: gitCredsStore,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient creates a new git client for the repository.
|
||||
func (r *repoClientFactory) NewClient(repo *v1alpha1.Repository, rootPath string) (git.Client, error) {
|
||||
gitCreds := repo.GetGitCreds(r.gitCredsStore)
|
||||
opts := git.WithEventHandlers(metrics.NewGitClientEventHandlers(r.metricsServer))
|
||||
return git.NewClientExt(repo.Repo, rootPath, gitCreds, repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy, repo.NoProxy, opts)
|
||||
}
|
||||
34
commitserver/metrics/githandlers.go
Normal file
34
commitserver/metrics/githandlers.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
)
|
||||
|
||||
// NewGitClientEventHandlers creates event handlers that update Git related metrics
|
||||
func NewGitClientEventHandlers(metricsServer *Server) git.EventHandlers {
|
||||
return git.EventHandlers{
|
||||
OnFetch: func(repo string) func() {
|
||||
startTime := time.Now()
|
||||
metricsServer.IncGitRequest(repo, GitRequestTypeFetch)
|
||||
return func() {
|
||||
metricsServer.ObserveGitRequestDuration(repo, GitRequestTypeFetch, time.Since(startTime))
|
||||
}
|
||||
},
|
||||
OnLsRemote: func(repo string) func() {
|
||||
startTime := time.Now()
|
||||
metricsServer.IncGitRequest(repo, GitRequestTypeLsRemote)
|
||||
return func() {
|
||||
metricsServer.ObserveGitRequestDuration(repo, GitRequestTypeLsRemote, time.Since(startTime))
|
||||
}
|
||||
},
|
||||
OnPush: func(repo string) func() {
|
||||
startTime := time.Now()
|
||||
metricsServer.IncGitRequest(repo, GitRequestTypePush)
|
||||
return func() {
|
||||
metricsServer.ObserveGitRequestDuration(repo, GitRequestTypePush, time.Since(startTime))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
157
commitserver/metrics/metrics.go
Normal file
157
commitserver/metrics/metrics.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Server is a prometheus server which collects application metrics.
|
||||
type Server struct {
|
||||
handler http.Handler
|
||||
commitPendingRequestsGauge *prometheus.GaugeVec
|
||||
gitRequestCounter *prometheus.CounterVec
|
||||
gitRequestHistogram *prometheus.HistogramVec
|
||||
commitRequestHistogram *prometheus.HistogramVec
|
||||
userInfoRequestHistogram *prometheus.HistogramVec
|
||||
commitRequestCounter *prometheus.CounterVec
|
||||
}
|
||||
|
||||
// GitRequestType is the type of git request
|
||||
type GitRequestType string
|
||||
|
||||
const (
|
||||
// GitRequestTypeLsRemote is a request to list remote refs
|
||||
GitRequestTypeLsRemote = "ls-remote"
|
||||
// GitRequestTypeFetch is a request to fetch from remote
|
||||
GitRequestTypeFetch = "fetch"
|
||||
// GitRequestTypePush is a request to push to remote
|
||||
GitRequestTypePush = "push"
|
||||
)
|
||||
|
||||
// CommitResponseType is the type of response for a commit request
|
||||
type CommitResponseType string
|
||||
|
||||
const (
|
||||
// CommitResponseTypeSuccess is a successful commit request
|
||||
CommitResponseTypeSuccess CommitResponseType = "success"
|
||||
// CommitResponseTypeFailure is a failed commit request
|
||||
CommitResponseTypeFailure CommitResponseType = "failure"
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics.
|
||||
func NewMetricsServer() *Server {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||
registry.MustRegister(collectors.NewGoCollector())
|
||||
|
||||
commitPendingRequestsGauge := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "argocd_commitserver_commit_pending_request_total",
|
||||
Help: "Number of pending commit requests",
|
||||
},
|
||||
[]string{"repo"},
|
||||
)
|
||||
registry.MustRegister(commitPendingRequestsGauge)
|
||||
|
||||
gitRequestCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_commitserver_git_request_total",
|
||||
Help: "Number of git requests performed by repo server",
|
||||
},
|
||||
[]string{"repo", "request_type"},
|
||||
)
|
||||
registry.MustRegister(gitRequestCounter)
|
||||
|
||||
gitRequestHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_commitserver_git_request_duration_seconds",
|
||||
Help: "Git requests duration seconds.",
|
||||
Buckets: []float64{0.1, 0.25, .5, 1, 2, 4, 10, 20},
|
||||
},
|
||||
[]string{"repo", "request_type"},
|
||||
)
|
||||
registry.MustRegister(gitRequestHistogram)
|
||||
|
||||
commitRequestHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_commitserver_commit_request_duration_seconds",
|
||||
Help: "Commit request duration seconds.",
|
||||
Buckets: []float64{0.1, 0.25, .5, 1, 2, 4, 10, 20},
|
||||
},
|
||||
[]string{"repo", "response_type"},
|
||||
)
|
||||
registry.MustRegister(commitRequestHistogram)
|
||||
|
||||
userInfoRequestHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_commitserver_userinfo_request_duration_seconds",
|
||||
Help: "Userinfo request duration seconds.",
|
||||
Buckets: []float64{0.1, 0.25, .5, 1, 2, 4, 10, 20},
|
||||
},
|
||||
[]string{"repo", "credential_type"},
|
||||
)
|
||||
registry.MustRegister(userInfoRequestHistogram)
|
||||
|
||||
commitRequestCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_commitserver_commit_request_total",
|
||||
Help: "Number of commit requests performed handled",
|
||||
},
|
||||
[]string{"repo", "response_type"},
|
||||
)
|
||||
registry.MustRegister(commitRequestCounter)
|
||||
|
||||
return &Server{
|
||||
handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{}),
|
||||
commitPendingRequestsGauge: commitPendingRequestsGauge,
|
||||
gitRequestCounter: gitRequestCounter,
|
||||
gitRequestHistogram: gitRequestHistogram,
|
||||
commitRequestHistogram: commitRequestHistogram,
|
||||
userInfoRequestHistogram: userInfoRequestHistogram,
|
||||
commitRequestCounter: commitRequestCounter,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandler returns the http.Handler for the prometheus server
|
||||
func (m *Server) GetHandler() http.Handler {
|
||||
return m.handler
|
||||
}
|
||||
|
||||
// IncPendingCommitRequest increments the pending commit requests gauge
|
||||
func (m *Server) IncPendingCommitRequest(repo string) {
|
||||
m.commitPendingRequestsGauge.WithLabelValues(repo).Inc()
|
||||
}
|
||||
|
||||
// DecPendingCommitRequest decrements the pending commit requests gauge
|
||||
func (m *Server) DecPendingCommitRequest(repo string) {
|
||||
m.commitPendingRequestsGauge.WithLabelValues(repo).Dec()
|
||||
}
|
||||
|
||||
// IncGitRequest increments the git requests counter
|
||||
func (m *Server) IncGitRequest(repo string, requestType GitRequestType) {
|
||||
m.gitRequestCounter.WithLabelValues(repo, string(requestType)).Inc()
|
||||
}
|
||||
|
||||
// ObserveGitRequestDuration observes the duration of a git request
|
||||
func (m *Server) ObserveGitRequestDuration(repo string, requestType GitRequestType, duration time.Duration) {
|
||||
m.gitRequestHistogram.WithLabelValues(repo, string(requestType)).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// ObserveCommitRequestDuration observes the duration of a commit request
|
||||
func (m *Server) ObserveCommitRequestDuration(repo string, rt CommitResponseType, duration time.Duration) {
|
||||
m.commitRequestHistogram.WithLabelValues(repo, string(rt)).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// ObserveUserInfoRequestDuration observes the duration of a userinfo request
|
||||
func (m *Server) ObserveUserInfoRequestDuration(repo string, credentialType string, duration time.Duration) {
|
||||
m.userInfoRequestHistogram.WithLabelValues(repo, credentialType).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// IncCommitRequest increments the commit request counter
|
||||
func (m *Server) IncCommitRequest(repo string, rt CommitResponseType) {
|
||||
m.commitRequestCounter.WithLabelValues(repo, string(rt)).Inc()
|
||||
}
|
||||
38
commitserver/server.go
Normal file
38
commitserver/server.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package commitserver
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/health"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/commit"
|
||||
"github.com/argoproj/argo-cd/v2/commitserver/metrics"
|
||||
versionpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/version"
|
||||
"github.com/argoproj/argo-cd/v2/server/version"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
)
|
||||
|
||||
// ArgoCDCommitServer is the server that handles commit requests.
|
||||
type ArgoCDCommitServer struct {
|
||||
commitService *commit.Service
|
||||
}
|
||||
|
||||
// NewServer returns a new instance of the commit server.
|
||||
func NewServer(gitCredsStore git.CredsStore, metricsServer *metrics.Server) *ArgoCDCommitServer {
|
||||
return &ArgoCDCommitServer{commitService: commit.NewService(gitCredsStore, metricsServer)}
|
||||
}
|
||||
|
||||
// CreateGRPC creates a new gRPC server.
|
||||
func (a *ArgoCDCommitServer) CreateGRPC() *grpc.Server {
|
||||
server := grpc.NewServer()
|
||||
versionpkg.RegisterVersionServiceServer(server, version.NewServer(nil, func() (bool, error) {
|
||||
return true, nil
|
||||
}))
|
||||
apiclient.RegisterCommitServiceServer(server, a.commitService)
|
||||
|
||||
healthService := health.NewServer()
|
||||
grpc_health_v1.RegisterHealthServer(server, healthService)
|
||||
|
||||
return server
|
||||
}
|
||||
@@ -26,6 +26,8 @@ const (
|
||||
const (
|
||||
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
|
||||
DefaultRepoServerAddr = "argocd-repo-server:8081"
|
||||
// DefaultCommitServerAddr is the gRPC address of the Argo CD commit server
|
||||
DefaultCommitServerAddr = "argocd-commit-server:8086"
|
||||
// DefaultDexServerAddr is the HTTP address of the Dex OIDC server, which we run a reverse proxy against
|
||||
DefaultDexServerAddr = "argocd-dex-server:5556"
|
||||
// DefaultRedisAddr is the default redis address
|
||||
@@ -62,15 +64,19 @@ const (
|
||||
DefaultPortArgoCDMetrics = 8082
|
||||
DefaultPortArgoCDAPIServerMetrics = 8083
|
||||
DefaultPortRepoServerMetrics = 8084
|
||||
DefaultPortCommitServer = 8086
|
||||
DefaultPortCommitServerMetrics = 8087
|
||||
)
|
||||
|
||||
// DefaultAddressAPIServer for ArgoCD components
|
||||
const (
|
||||
DefaultAddressAdminDashboard = "localhost"
|
||||
DefaultAddressAPIServer = "0.0.0.0"
|
||||
DefaultAddressAPIServerMetrics = "0.0.0.0"
|
||||
DefaultAddressRepoServer = "0.0.0.0"
|
||||
DefaultAddressRepoServerMetrics = "0.0.0.0"
|
||||
DefaultAddressAdminDashboard = "localhost"
|
||||
DefaultAddressAPIServer = "0.0.0.0"
|
||||
DefaultAddressAPIServerMetrics = "0.0.0.0"
|
||||
DefaultAddressRepoServer = "0.0.0.0"
|
||||
DefaultAddressRepoServerMetrics = "0.0.0.0"
|
||||
DefaultAddressCommitServer = "0.0.0.0"
|
||||
DefaultAddressCommitServerMetrics = "0.0.0.0"
|
||||
)
|
||||
|
||||
// Default paths on the pod's file system
|
||||
@@ -175,6 +181,8 @@ const (
|
||||
LabelValueSecretTypeRepository = "repository"
|
||||
// LabelValueSecretTypeRepoCreds indicates a secret type of repository credentials
|
||||
LabelValueSecretTypeRepoCreds = "repo-creds"
|
||||
// LabelValueSecretTypeRepositoryWrite indicates a secret type of repository credentials for writing
|
||||
LabelValueSecretTypeRepositoryWrite = "repository-write"
|
||||
// LabelValueSecretTypeSCMCreds indicates a secret type of SCM credentials
|
||||
LabelValueSecretTypeSCMCreds = "scm-creds"
|
||||
|
||||
|
||||
@@ -42,8 +42,10 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
commitclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
statecache "github.com/argoproj/argo-cd/v2/controller/cache"
|
||||
"github.com/argoproj/argo-cd/v2/controller/hydrator"
|
||||
"github.com/argoproj/argo-cd/v2/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/controller/sharding"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application"
|
||||
@@ -121,6 +123,8 @@ type ApplicationController struct {
|
||||
appComparisonTypeRefreshQueue workqueue.TypedRateLimitingInterface[string]
|
||||
appOperationQueue workqueue.TypedRateLimitingInterface[string]
|
||||
projectRefreshQueue workqueue.TypedRateLimitingInterface[string]
|
||||
appHydrateQueue workqueue.TypedRateLimitingInterface[string]
|
||||
hydrationQueue workqueue.TypedRateLimitingInterface[hydrator.HydrationQueueKey]
|
||||
appInformer cache.SharedIndexInformer
|
||||
appLister applisters.ApplicationLister
|
||||
projInformer cache.SharedIndexInformer
|
||||
@@ -146,6 +150,8 @@ type ApplicationController struct {
|
||||
// dynamicClusterDistributionEnabled if disabled deploymentInformer is never initialized
|
||||
dynamicClusterDistributionEnabled bool
|
||||
deploymentInformer informerv1.DeploymentInformer
|
||||
|
||||
hydrator *hydrator.Hydrator
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
@@ -155,6 +161,7 @@ func NewApplicationController(
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset apiclient.Clientset,
|
||||
commitClientset commitclient.Clientset,
|
||||
argoCache *appstatecache.Cache,
|
||||
kubectl kube.Kubectl,
|
||||
appResyncPeriod time.Duration,
|
||||
@@ -177,6 +184,7 @@ func NewApplicationController(
|
||||
dynamicClusterDistributionEnabled bool,
|
||||
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
|
||||
enableK8sEvent []string,
|
||||
hydratorEnabled bool,
|
||||
) (*ApplicationController, error) {
|
||||
log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v, appResyncJitter=%v", appResyncPeriod, appHardResyncPeriod, appResyncJitter)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
@@ -190,10 +198,12 @@ func NewApplicationController(
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectl,
|
||||
applicationClientset: applicationClientset,
|
||||
appRefreshQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_reconciliation_queue"}),
|
||||
appOperationQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_operation_processing_queue"}),
|
||||
projectRefreshQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "project_reconciliation_queue"}),
|
||||
appComparisonTypeRefreshQueue: workqueue.NewTypedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig)),
|
||||
appRefreshQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_reconciliation_queue"}),
|
||||
appOperationQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_operation_processing_queue"}),
|
||||
projectRefreshQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "project_reconciliation_queue"}),
|
||||
appComparisonTypeRefreshQueue: workqueue.NewTypedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig)),
|
||||
appHydrateQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_hydration_queue"}),
|
||||
hydrationQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[hydrator.HydrationQueueKey](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[hydrator.HydrationQueueKey]{Name: "manifest_hydration_queue"}),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
statusHardRefreshTimeout: appHardResyncPeriod,
|
||||
@@ -211,6 +221,9 @@ func NewApplicationController(
|
||||
dynamicClusterDistributionEnabled: dynamicClusterDistributionEnabled,
|
||||
ignoreNormalizerOpts: ignoreNormalizerOpts,
|
||||
}
|
||||
if hydratorEnabled {
|
||||
ctrl.hydrator = hydrator.NewHydrator(&ctrl, appResyncPeriod, commitClientset)
|
||||
}
|
||||
if kubectlParallelismLimit > 0 {
|
||||
ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
|
||||
}
|
||||
@@ -845,6 +858,8 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
defer ctrl.appComparisonTypeRefreshQueue.ShutDown()
|
||||
defer ctrl.appOperationQueue.ShutDown()
|
||||
defer ctrl.projectRefreshQueue.ShutDown()
|
||||
defer ctrl.appHydrateQueue.ShutDown()
|
||||
defer ctrl.hydrationQueue.ShutDown()
|
||||
|
||||
ctrl.metricsServer.RegisterClustersInfoSource(ctx, ctrl.stateCache)
|
||||
ctrl.RegisterClusterSecretUpdater(ctx)
|
||||
@@ -903,6 +918,19 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
for ctrl.processProjectQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
|
||||
if ctrl.hydrator != nil {
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppHydrateQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
|
||||
go wait.Until(func() {
|
||||
for ctrl.processHydrationQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
@@ -1774,6 +1802,68 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppHydrateQueueItem() (processNext bool) {
|
||||
appKey, shutdown := ctrl.appHydrateQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
processNext = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appHydrateQueue.Done(appKey)
|
||||
}()
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
origApp, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
|
||||
ctrl.hydrator.ProcessAppHydrateQueueItem(origApp)
|
||||
|
||||
getAppLog(origApp).Debug("Successfully processed app hydrate queue item")
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processHydrationQueueItem() (processNext bool) {
|
||||
hydrationKey, shutdown := ctrl.hydrationQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
processNext = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.hydrationQueue.Done(hydrationKey)
|
||||
}()
|
||||
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"sourceRepoURL": hydrationKey.SourceRepoURL,
|
||||
"sourceTargetRevision": hydrationKey.SourceTargetRevision,
|
||||
"destinationBranch": hydrationKey.DestinationBranch,
|
||||
})
|
||||
|
||||
logCtx.Debug("Processing hydration queue item")
|
||||
|
||||
ctrl.hydrator.ProcessHydrationQueueItem(hydrationKey)
|
||||
|
||||
logCtx.Debug("Successfully processed hydration queue item")
|
||||
return
|
||||
}
|
||||
|
||||
func resourceStatusKey(res appv1.ResourceStatus) string {
|
||||
return strings.Join([]string{res.Group, res.Kind, res.Namespace, res.Name}, "/")
|
||||
}
|
||||
@@ -1782,7 +1872,8 @@ func currentSourceEqualsSyncedSource(app *appv1.Application) bool {
|
||||
if app.Spec.HasMultipleSources() {
|
||||
return app.Spec.Sources.Equals(app.Status.Sync.ComparedTo.Sources)
|
||||
}
|
||||
return app.Spec.Source.Equals(&app.Status.Sync.ComparedTo.Source)
|
||||
source := app.Spec.GetSource()
|
||||
return source.Equals(&app.Status.Sync.ComparedTo.Source)
|
||||
}
|
||||
|
||||
// needRefreshAppStatus answers if application status needs to be refreshed.
|
||||
@@ -1918,6 +2009,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
newAnnotations[k] = v
|
||||
}
|
||||
delete(newAnnotations, appv1.AnnotationKeyRefresh)
|
||||
delete(newAnnotations, appv1.AnnotationKeyHydrate)
|
||||
}
|
||||
patch, modified, err := createMergePatch(
|
||||
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: orig.GetAnnotations()}, Status: orig.Status},
|
||||
@@ -2325,6 +2417,9 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
if !newOK || (delay != nil && *delay != time.Duration(0)) {
|
||||
ctrl.appOperationQueue.AddRateLimited(key)
|
||||
}
|
||||
if ctrl.hydrator != nil {
|
||||
ctrl.appHydrateQueue.AddRateLimited(newApp.QualifiedName())
|
||||
}
|
||||
ctrl.clusterSharding.UpdateApp(newApp)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
|
||||
@@ -42,6 +42,7 @@ import (
|
||||
|
||||
dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks"
|
||||
|
||||
mockcommitclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient/mocks"
|
||||
mockstatecache "github.com/argoproj/argo-cd/v2/controller/cache/mocks"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
|
||||
@@ -126,6 +127,8 @@ func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration,
|
||||
|
||||
mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient}
|
||||
|
||||
mockCommitClientset := mockcommitclient.Clientset{}
|
||||
|
||||
secret := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
@@ -157,6 +160,7 @@ func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration,
|
||||
kubeClient,
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
&mockCommitClientset,
|
||||
appstatecache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
@@ -182,6 +186,7 @@ func newFakeControllerWithResync(data *fakeData, appResyncPeriod time.Duration,
|
||||
false,
|
||||
normalizers.IgnoreNormalizerOpts{},
|
||||
testEnableEventList,
|
||||
false,
|
||||
)
|
||||
db := &dbmocks.ArgoDB{}
|
||||
db.On("GetApplicationControllerReplicas").Return(1)
|
||||
|
||||
@@ -51,7 +51,7 @@ func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Applicat
|
||||
revisions = append(revisions, src.TargetRevision)
|
||||
}
|
||||
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, false)
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, false, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
362
controller/hydrator/hydrator.go
Normal file
362
controller/hydrator/hydrator.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package hydrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
commitclient "github.com/argoproj/argo-cd/v2/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/controller/utils"
|
||||
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
argoio "github.com/argoproj/argo-cd/v2/util/io"
|
||||
)
|
||||
|
||||
// Dependencies is the interface for the dependencies of the Hydrator. It serves two purposes: 1) it prevents the
|
||||
// hydrator from having direct access to the app controller, and 2) it allows for easy mocking of dependencies in tests.
|
||||
// If you add something here, be sure that it is something the app controller needs to provide to the hydrator.
|
||||
type Dependencies interface {
|
||||
// TODO: determine if we actually need to get the app, or if all the stuff we need the app for is done already on
|
||||
// the app controller side.
|
||||
GetProcessableAppProj(app *appv1.Application) (*appv1.AppProject, error)
|
||||
GetProcessableApps() (*appv1.ApplicationList, error)
|
||||
GetRepoObjs(app *appv1.Application, source appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)
|
||||
GetWriteCredentials(ctx context.Context, repoURL string, project string) (*appv1.Repository, error)
|
||||
ResolveGitRevision(repoURL, targetRevision string) (string, error)
|
||||
RequestAppRefresh(appName string)
|
||||
// TODO: only allow access to the hydrator status
|
||||
PersistAppHydratorStatus(orig *appv1.Application, newStatus *appv1.SourceHydratorStatus)
|
||||
AddHydrationQueueItem(key HydrationQueueKey)
|
||||
}
|
||||
|
||||
type Hydrator struct {
|
||||
dependencies Dependencies
|
||||
statusRefreshTimeout time.Duration
|
||||
commitClientset commitclient.Clientset
|
||||
}
|
||||
|
||||
func NewHydrator(dependencies Dependencies, statusRefreshTimeout time.Duration, commitClientset commitclient.Clientset) *Hydrator {
|
||||
return &Hydrator{
|
||||
dependencies: dependencies,
|
||||
statusRefreshTimeout: statusRefreshTimeout,
|
||||
commitClientset: commitClientset,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hydrator) ProcessAppHydrateQueueItem(origApp *appv1.Application) {
|
||||
origApp = origApp.DeepCopy()
|
||||
app := origApp.DeepCopy()
|
||||
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logCtx := utils.GetAppLog(app)
|
||||
|
||||
logCtx.Debug("Processing app hydrate queue item")
|
||||
|
||||
// If we're using a source hydrator, see if the dry source has changed.
|
||||
latestRevision, err := h.dependencies.ResolveGitRevision(app.Spec.SourceHydrator.DrySource.RepoURL, app.Spec.SourceHydrator.DrySource.TargetRevision)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to check whether dry source has changed, skipping: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: don't reuse statusRefreshTimeout. Create a new timeout for hydration.
|
||||
needsHydration, reason := appNeedsHydration(origApp, h.statusRefreshTimeout, latestRevision)
|
||||
if !needsHydration {
|
||||
return
|
||||
}
|
||||
if latestRevision == "" {
|
||||
logCtx.Errorf("Dry source has not been resolved, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
logCtx.WithField("reason", reason).Info("Hydrating app")
|
||||
|
||||
app.Status.SourceHydrator.CurrentOperation = &appv1.HydrateOperation{
|
||||
DrySHA: latestRevision,
|
||||
StartedAt: metav1.Now(),
|
||||
FinishedAt: nil,
|
||||
Phase: appv1.HydrateOperationPhaseHydrating,
|
||||
SourceHydrator: *app.Spec.SourceHydrator,
|
||||
}
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
origApp.Status.SourceHydrator = app.Status.SourceHydrator
|
||||
h.dependencies.AddHydrationQueueItem(getHydrationQueueKey(app))
|
||||
|
||||
logCtx.Debug("Successfully processed app hydrate queue item")
|
||||
}
|
||||
|
||||
func getHydrationQueueKey(app *appv1.Application) HydrationQueueKey {
|
||||
destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
if app.Spec.SourceHydrator.HydrateTo != nil {
|
||||
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetBranch
|
||||
}
|
||||
key := HydrationQueueKey{
|
||||
SourceRepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
|
||||
SourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
DestinationBranch: destinationBranch,
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
type HydrationQueueKey struct {
|
||||
SourceRepoURL string
|
||||
SourceTargetRevision string
|
||||
DestinationBranch string
|
||||
}
|
||||
|
||||
// uniqueHydrationDestination is used to detect duplicate hydrate destinations.
|
||||
type uniqueHydrationDestination struct {
|
||||
sourceRepoURL string
|
||||
sourceTargetRevision string
|
||||
destinationBranch string
|
||||
destinationPath string
|
||||
}
|
||||
|
||||
func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey HydrationQueueKey) (processNext bool) {
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"sourceRepoURL": hydrationKey.SourceRepoURL,
|
||||
"sourceTargetRevision": hydrationKey.SourceTargetRevision,
|
||||
"destinationBranch": hydrationKey.DestinationBranch,
|
||||
})
|
||||
|
||||
relevantApps, drySHA, hydratedSHA, err := h.hydrateAppsLatestCommit(logCtx, hydrationKey)
|
||||
if drySHA != "" {
|
||||
logCtx = logCtx.WithField("drySHA", drySHA)
|
||||
}
|
||||
if err != nil {
|
||||
logCtx.WithField("appCount", len(relevantApps)).WithError(err).Error("Failed to hydrate apps")
|
||||
for _, app := range relevantApps {
|
||||
origApp := app.DeepCopy()
|
||||
app.Status.SourceHydrator.CurrentOperation.Phase = appv1.HydrateOperationPhaseFailed
|
||||
failedAt := metav1.Now()
|
||||
app.Status.SourceHydrator.CurrentOperation.FinishedAt = &failedAt
|
||||
app.Status.SourceHydrator.CurrentOperation.Message = fmt.Sprintf("Failed to hydrated revision %s: %v", drySHA, err.Error())
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
logCtx = logCtx.WithField("app", app.QualifiedName())
|
||||
logCtx.Errorf("Failed to hydrate app: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
logCtx.WithField("appCount", len(relevantApps)).Debug("Successfully hydrated apps")
|
||||
finishedAt := metav1.Now()
|
||||
for _, app := range relevantApps {
|
||||
origApp := app.DeepCopy()
|
||||
operation := &appv1.HydrateOperation{
|
||||
StartedAt: app.Status.SourceHydrator.CurrentOperation.StartedAt,
|
||||
FinishedAt: &finishedAt,
|
||||
Phase: appv1.HydrateOperationPhaseHydrated,
|
||||
Message: "",
|
||||
DrySHA: drySHA,
|
||||
HydratedSHA: hydratedSHA,
|
||||
SourceHydrator: app.Status.SourceHydrator.CurrentOperation.SourceHydrator,
|
||||
}
|
||||
app.Status.SourceHydrator.CurrentOperation = operation
|
||||
app.Status.SourceHydrator.LastSuccessfulOperation = &appv1.SuccessfulHydrateOperation{
|
||||
DrySHA: drySHA,
|
||||
HydratedSHA: hydratedSHA,
|
||||
SourceHydrator: app.Status.SourceHydrator.CurrentOperation.SourceHydrator,
|
||||
}
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
// Request a refresh since we pushed a new commit.
|
||||
h.dependencies.RequestAppRefresh(app.QualifiedName())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey HydrationQueueKey) ([]*appv1.Application, string, string, error) {
|
||||
relevantApps, err := h.getRelevantAppsForHydration(logCtx, hydrationKey)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("failed to get relevant apps for hydration: %w", err)
|
||||
}
|
||||
|
||||
dryRevision, err := h.dependencies.ResolveGitRevision(hydrationKey.SourceRepoURL, hydrationKey.SourceTargetRevision)
|
||||
if err != nil {
|
||||
return relevantApps, "", "", fmt.Errorf("failed to resolve dry revision: %w", err)
|
||||
}
|
||||
|
||||
hydratedRevision, err := h.hydrate(logCtx, relevantApps, dryRevision)
|
||||
if err != nil {
|
||||
return relevantApps, dryRevision, "", fmt.Errorf("failed to hydrate apps: %w", err)
|
||||
}
|
||||
|
||||
return relevantApps, dryRevision, hydratedRevision, nil
|
||||
}
|
||||
|
||||
func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey HydrationQueueKey) ([]*appv1.Application, error) {
|
||||
// Get all apps
|
||||
apps, err := h.dependencies.GetProcessableApps()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list apps: %w", err)
|
||||
}
|
||||
|
||||
var relevantApps []*appv1.Application
|
||||
uniqueDestinations := make(map[uniqueHydrationDestination]bool, len(apps.Items))
|
||||
for _, app := range apps.Items {
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if app.Spec.SourceHydrator.DrySource.RepoURL != hydrationKey.SourceRepoURL ||
|
||||
app.Spec.SourceHydrator.DrySource.TargetRevision != hydrationKey.SourceTargetRevision {
|
||||
continue
|
||||
}
|
||||
destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
if app.Spec.SourceHydrator.HydrateTo != nil {
|
||||
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetBranch
|
||||
}
|
||||
if destinationBranch != hydrationKey.DestinationBranch {
|
||||
continue
|
||||
}
|
||||
|
||||
var proj *appv1.AppProject
|
||||
proj, err = h.dependencies.GetProcessableAppProj(&app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project %q for app %q: %w", app.Spec.Project, app.QualifiedName(), err)
|
||||
}
|
||||
permitted := proj.IsSourcePermitted(app.Spec.GetSource())
|
||||
if !permitted {
|
||||
// Log and skip. We don't want to fail the entire operation because of one app.
|
||||
logCtx.Warnf("App %q is not permitted to use source %q", app.QualifiedName(), app.Spec.Source.String())
|
||||
continue
|
||||
}
|
||||
|
||||
uniqueDestinationKey := uniqueHydrationDestination{
|
||||
sourceRepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
|
||||
sourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
destinationBranch: destinationBranch,
|
||||
destinationPath: app.Spec.SourceHydrator.SyncSource.Path,
|
||||
}
|
||||
// TODO: test the dupe detection
|
||||
if _, ok := uniqueDestinations[uniqueDestinationKey]; ok {
|
||||
return nil, fmt.Errorf("multiple app hydrators use the same destination: %v", uniqueDestinationKey)
|
||||
}
|
||||
uniqueDestinations[uniqueDestinationKey] = true
|
||||
|
||||
relevantApps = append(relevantApps, &app)
|
||||
}
|
||||
return relevantApps, nil
|
||||
}
|
||||
|
||||
func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application, revision string) (string, error) {
|
||||
if len(apps) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
repoURL := apps[0].Spec.SourceHydrator.DrySource.RepoURL
|
||||
syncBranch := apps[0].Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
targetBranch := apps[0].Spec.GetHydrateToSource().TargetRevision
|
||||
var paths []*commitclient.PathDetails
|
||||
projects := make(map[string]bool, len(apps))
|
||||
// TODO: parallelize this loop
|
||||
for _, app := range apps {
|
||||
project, err := h.dependencies.GetProcessableAppProj(app)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get project: %w", err)
|
||||
}
|
||||
projects[project.Name] = true
|
||||
drySource := appv1.ApplicationSource{
|
||||
RepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
|
||||
Path: app.Spec.SourceHydrator.DrySource.Path,
|
||||
TargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
}
|
||||
targetRevision := app.Spec.SourceHydrator.DrySource.TargetRevision
|
||||
|
||||
// TODO: enable signature verification
|
||||
objs, resp, err := h.dependencies.GetRepoObjs(app, drySource, targetRevision, project)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get repo objects: %w", err)
|
||||
}
|
||||
|
||||
// Set up a ManifestsRequest
|
||||
manifestDetails := make([]*commitclient.HydratedManifestDetails, len(objs))
|
||||
for i, obj := range objs {
|
||||
objJson, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal object: %w", err)
|
||||
}
|
||||
manifestDetails[i] = &commitclient.HydratedManifestDetails{ManifestJSON: string(objJson)}
|
||||
}
|
||||
|
||||
paths = append(paths, &commitclient.PathDetails{
|
||||
Path: app.Spec.SourceHydrator.SyncSource.Path,
|
||||
Manifests: manifestDetails,
|
||||
Commands: resp.Commands,
|
||||
})
|
||||
}
|
||||
|
||||
// If all the apps are under the same project, use that project. Otherwise, use an empty string to indicate that we
|
||||
// need global creds.
|
||||
project := ""
|
||||
if len(projects) == 1 {
|
||||
for p := range projects {
|
||||
project = p
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := h.dependencies.GetWriteCredentials(context.Background(), repoURL, project)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get hydrator credentials: %w", err)
|
||||
}
|
||||
if repo == nil {
|
||||
// Try without credentials.
|
||||
repo = &appv1.Repository{
|
||||
Repo: repoURL,
|
||||
}
|
||||
logCtx.Warn("no credentials found for repo, continuing without credentials")
|
||||
}
|
||||
|
||||
manifestsRequest := commitclient.CommitHydratedManifestsRequest{
|
||||
Repo: repo,
|
||||
SyncBranch: syncBranch,
|
||||
TargetBranch: targetBranch,
|
||||
DrySha: revision,
|
||||
CommitMessage: fmt.Sprintf("[Argo CD Bot] hydrate %s", revision),
|
||||
Paths: paths,
|
||||
}
|
||||
|
||||
closer, commitService, err := h.commitClientset.NewCommitServerClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create commit service: %w", err)
|
||||
}
|
||||
defer argoio.Close(closer)
|
||||
resp, err := commitService.CommitHydratedManifests(context.Background(), &manifestsRequest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to commit hydrated manifests: %w", err)
|
||||
}
|
||||
return resp.HydratedSha, nil
|
||||
}
|
||||
|
||||
// appNeedsHydration answers if application needs manifests hydrated.
|
||||
func appNeedsHydration(app *appv1.Application, statusHydrateTimeout time.Duration, latestRevision string) (needsHydration bool, reason string) {
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
return false, "source hydrator not configured"
|
||||
}
|
||||
|
||||
var hydratedAt *metav1.Time
|
||||
if app.Status.SourceHydrator.CurrentOperation != nil {
|
||||
hydratedAt = &app.Status.SourceHydrator.CurrentOperation.StartedAt
|
||||
}
|
||||
|
||||
if app.IsHydrateRequested() {
|
||||
return true, "hydrate requested"
|
||||
} else if app.Status.SourceHydrator.CurrentOperation == nil {
|
||||
return true, "no previous hydrate operation"
|
||||
} else if !app.Spec.SourceHydrator.DeepEquals(app.Status.SourceHydrator.CurrentOperation.SourceHydrator) {
|
||||
return true, "spec.sourceHydrator differs"
|
||||
} else if app.Status.SourceHydrator.CurrentOperation.DrySHA != latestRevision {
|
||||
return true, "revision differs"
|
||||
} else if app.Status.SourceHydrator.CurrentOperation.Phase == appv1.HydrateOperationPhaseFailed && metav1.Now().Sub(app.Status.SourceHydrator.CurrentOperation.FinishedAt.Time) > 2*time.Minute {
|
||||
return true, "previous hydrate operation failed more than 2 minutes ago"
|
||||
} else if hydratedAt == nil || hydratedAt.Add(statusHydrateTimeout).Before(time.Now().UTC()) {
|
||||
return true, "hydration expired"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
114
controller/hydrator/hydrator_test.go
Normal file
114
controller/hydrator/hydrator_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package hydrator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_appNeedsHydration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
now := metav1.NewTime(time.Now())
|
||||
oneHourAgo := metav1.NewTime(now.Add(-1 * time.Hour))
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
app *v1alpha1.Application
|
||||
timeout time.Duration
|
||||
latestRevision string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "source hydrator not configured",
|
||||
app: &v1alpha1.Application{},
|
||||
expected: "source hydrator not configured",
|
||||
},
|
||||
{
|
||||
name: "hydrate requested",
|
||||
app: &v1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{v1alpha1.AnnotationKeyHydrate: "normal"}},
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "hydrate requested",
|
||||
},
|
||||
{
|
||||
name: "no previous hydrate operation",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "no previous hydrate operation",
|
||||
},
|
||||
{
|
||||
name: "spec.sourceHydrator differs",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{
|
||||
SourceHydrator: v1alpha1.SourceHydrator{DrySource: v1alpha1.DrySource{RepoURL: "something new"}},
|
||||
}}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "spec.sourceHydrator differs",
|
||||
},
|
||||
{
|
||||
name: "dry SHA has changed",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{DrySHA: "xyz123"}}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "revision differs",
|
||||
},
|
||||
{
|
||||
name: "hydration failed more than two minutes ago",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{DrySHA: "abc123", FinishedAt: &oneHourAgo, Phase: v1alpha1.HydrateOperationPhaseFailed}}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "previous hydrate operation failed more than 2 minutes ago",
|
||||
},
|
||||
{
|
||||
name: "timeout reached",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{StartedAt: oneHourAgo}}},
|
||||
},
|
||||
timeout: 1 * time.Minute,
|
||||
latestRevision: "",
|
||||
expected: "hydration expired",
|
||||
},
|
||||
{
|
||||
name: "hydrate not needed",
|
||||
app: &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{SourceHydrator: &v1alpha1.SourceHydrator{}},
|
||||
Status: v1alpha1.ApplicationStatus{SourceHydrator: v1alpha1.SourceHydratorStatus{CurrentOperation: &v1alpha1.HydrateOperation{DrySHA: "abc123", StartedAt: now, FinishedAt: &now, Phase: v1alpha1.HydrateOperationPhaseFailed}}},
|
||||
},
|
||||
timeout: 1 * time.Hour,
|
||||
latestRevision: "abc123",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
needsHydration, result := appNeedsHydration(tc.app, tc.timeout, tc.latestRevision)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
if !needsHydration {
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
75
controller/hydrator_dependencies.go
Normal file
75
controller/hydrator_dependencies.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/controller/hydrator"
|
||||
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
/**
|
||||
This file implements the hydrator.Dependencies interface for the ApplicationController.
|
||||
|
||||
Hydration logic does not belong in this file. The methods here should be "bookkeeping" methods that keep hydration work
|
||||
in the hydrator and app controller work in the app controller. The only purpose of this file is to provide the hydrator
|
||||
safe, minimal access to certain app controller functionality to avoid duplicate code.
|
||||
*/
|
||||
|
||||
func (ctrl *ApplicationController) GetProcessableAppProj(app *appv1.Application) (*appv1.AppProject, error) {
|
||||
return ctrl.getAppProj(app)
|
||||
}
|
||||
|
||||
// GetProcessableApps returns a list of applications that are processable by the controller.
|
||||
func (ctrl *ApplicationController) GetProcessableApps() (*appv1.ApplicationList, error) {
|
||||
// getAppList already filters out applications that are not processable by the controller.
|
||||
return ctrl.getAppList(metav1.ListOptions{})
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) GetRepoObjs(app *appv1.Application, source appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
sources := []appv1.ApplicationSource{source}
|
||||
revisions := []string{revision}
|
||||
|
||||
appLabelKey, err := ctrl.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get app instance label key: %w", err)
|
||||
}
|
||||
|
||||
// FIXME: use cache and revision cache
|
||||
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(app, sources, appLabelKey, revisions, true, true, false, project, false, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get repo objects: %w", err)
|
||||
}
|
||||
|
||||
if len(resp) != 1 {
|
||||
return nil, nil, fmt.Errorf("expected one manifest response, got %d", len(resp))
|
||||
}
|
||||
|
||||
return objs, resp[0], nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) GetWriteCredentials(ctx context.Context, repoURL string, project string) (*appv1.Repository, error) {
|
||||
return ctrl.db.GetWriteRepository(ctx, repoURL, project)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) ResolveGitRevision(repoURL, targetRevision string) (string, error) {
|
||||
return ctrl.appStateManager.ResolveGitRevision(repoURL, targetRevision)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) RequestAppRefresh(appName string) {
|
||||
ctrl.requestAppRefresh(appName, CompareWithLatest.Pointer(), nil)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) PersistAppHydratorStatus(orig *appv1.Application, newStatus *appv1.SourceHydratorStatus) {
|
||||
status := orig.Status.DeepCopy()
|
||||
status.SourceHydrator = *newStatus
|
||||
ctrl.persistAppStatus(orig, status)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) AddHydrationQueueItem(key hydrator.HydrationQueueKey) {
|
||||
ctrl.hydrationQueue.AddRateLimited(key)
|
||||
}
|
||||
1
controller/log_utils.go
Normal file
1
controller/log_utils.go
Normal file
@@ -0,0 +1 @@
|
||||
package controller
|
||||
@@ -71,7 +71,8 @@ type managedResource struct {
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool, rollback bool) (*comparisonResult, error)
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
ResolveGitRevision(repoURL string, revision string) (string, error)
|
||||
}
|
||||
|
||||
// comparisonResult holds the state of an application after the reconciliation
|
||||
@@ -125,7 +126,7 @@ type appStateManager struct {
|
||||
// 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(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, rollback, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
@@ -219,6 +220,14 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
|
||||
revision := revisions[i]
|
||||
|
||||
appNamespace := app.Spec.Destination.Namespace
|
||||
apiVersions := argo.APIResourcesToStrings(apiResources, true)
|
||||
if !sendRuntimeState {
|
||||
appNamespace = ""
|
||||
apiVersions = nil
|
||||
serverVersion = ""
|
||||
}
|
||||
|
||||
if !source.IsHelm() && syncedRevision != "" && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" {
|
||||
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
|
||||
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(context.Background(), &apiclient.UpdateRevisionForPathsRequest{
|
||||
@@ -229,10 +238,10 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
Paths: path.GetAppRefreshPaths(app),
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.InstanceName(m.namespace),
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
Namespace: appNamespace,
|
||||
ApplicationSource: &source,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIResourcesToStrings(apiResources, true),
|
||||
ApiVersions: apiVersions,
|
||||
TrackingMethod: string(argo.GetTrackingMethod(m.settingsMgr)),
|
||||
RefSources: refSources,
|
||||
HasMultipleSources: app.Spec.HasMultipleSources(),
|
||||
@@ -263,11 +272,11 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
NoRevisionCache: noRevisionCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.InstanceName(m.namespace),
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
Namespace: appNamespace,
|
||||
ApplicationSource: &source,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIResourcesToStrings(apiResources, true),
|
||||
ApiVersions: apiVersions,
|
||||
VerifySignature: verifySignature,
|
||||
HelmRepoCreds: permittedHelmCredentials,
|
||||
TrackingMethod: string(argo.GetTrackingMethod(m.settingsMgr)),
|
||||
@@ -309,6 +318,39 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
return targetObjs, manifestInfos, revisionUpdated, nil
|
||||
}
|
||||
|
||||
// ResolveGitRevision will resolve the given revision to a full commit SHA. Only works for git.
|
||||
func (m *appStateManager) ResolveGitRevision(repoURL string, revision string) (string, error) {
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to connect to repo server: %w", err)
|
||||
}
|
||||
defer io.Close(conn)
|
||||
|
||||
repo, err := m.db.GetRepository(context.Background(), repoURL, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get repo %q: %w", repoURL, err)
|
||||
}
|
||||
|
||||
// Mock the app. The repo-server only needs to know whether the "chart" field is populated.
|
||||
app := &v1alpha1.Application{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Source: &v1alpha1.ApplicationSource{
|
||||
RepoURL: repoURL,
|
||||
TargetRevision: revision,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := repoClient.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{
|
||||
Repo: repo,
|
||||
App: app,
|
||||
AmbiguousRevision: revision,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to determine whether the dry source has changed: %w", err)
|
||||
}
|
||||
return resp.Revision, nil
|
||||
}
|
||||
|
||||
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifests {
|
||||
@@ -490,7 +532,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
}
|
||||
}
|
||||
|
||||
targetObjs, manifestInfos, revisionUpdated, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, rollback)
|
||||
targetObjs, manifestInfos, revisionUpdated, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, rollback, true)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
msg := fmt.Sprintf("Failed to load target state: %s", err.Error())
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestPersistRevisionHistory(t *testing.T) {
|
||||
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, updatedApp.Status.History, 1)
|
||||
require.Len(t, updatedApp.Status.History, 1)
|
||||
assert.Equal(t, app.Spec.GetSource(), updatedApp.Status.History[0].Source)
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
}
|
||||
|
||||
17
controller/utils/log.go
Normal file
17
controller/utils/log.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// GetAppLog returns a logrus entry with fields set for the given application.
|
||||
func GetAppLog(app *v1alpha1.Application) *logrus.Entry {
|
||||
return logrus.WithFields(logrus.Fields{
|
||||
"application": app.Name,
|
||||
"app-namespace": app.Namespace,
|
||||
"app-qualified-name": app.QualifiedName(),
|
||||
"project": app.Spec.Project,
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,9 @@ data:
|
||||
# Repo server address. (default "argocd-repo-server:8081")
|
||||
repo.server: "argocd-repo-server:8081"
|
||||
|
||||
# Commit server address. (default "argocd-commit-server:8086")
|
||||
commit.server: "argocd-commit-server:8086"
|
||||
|
||||
# Redis server hostname and port (e.g. argocd-redis:6379)
|
||||
redis.server: "argocd-redis:6379"
|
||||
# Enable compression for data sent to Redis with the required compression algorithm. (default 'gzip')
|
||||
@@ -16,6 +19,9 @@ data:
|
||||
# Redis database
|
||||
redis.db:
|
||||
|
||||
# Enables the alpha "manifest hydrator" feature. (default "false")
|
||||
hydrator.enabled: "false"
|
||||
|
||||
# Open-Telemetry collector address: (e.g. "otel-collector:4317")
|
||||
otlp.address: ""
|
||||
# Open-Telemetry collector insecure: (e.g. "true")
|
||||
@@ -195,6 +201,15 @@ data:
|
||||
# Include hidden directories from Git
|
||||
reposerver.include.hidden.directories: "false"
|
||||
|
||||
## Commit-server properties
|
||||
# Listen on given address for incoming connections (default "0.0.0.0")
|
||||
commitserver.listen.address: "0.0.0.0"
|
||||
# Set the logging format. One of: text|json (default "text")
|
||||
commitserver.log.format: "text"
|
||||
# Set the logging level. One of: debug|info|warn|error (default "info")
|
||||
commitserver.log.level: "info"
|
||||
# Listen on given address for metrics (default "0.0.0.0")
|
||||
commitserver.metrics.listen.address: "0.0.0.0"
|
||||
|
||||
# Set the logging format. One of: text|json (default "text")
|
||||
dexserver.log.format: "text"
|
||||
|
||||
@@ -129,6 +129,20 @@ Scraped at the `argocd-repo-server:8084/metrics` endpoint.
|
||||
| `argocd_redis_request_total` | counter | Number of Kubernetes requests executed during application reconciliation. |
|
||||
| `argocd_repo_pending_request_total` | gauge | Number of pending requests requiring repository lock |
|
||||
|
||||
## Commit Server Metrics
|
||||
|
||||
Metrics about the Commit Server.
|
||||
Scraped at the `argocd-commit-server:8087/metrics` endpoint.
|
||||
|
||||
| Metric | Type | Description |
|
||||
|---------------------------------------------------------|:---------:|------------------------------------------------------|
|
||||
| `argocd_commitserver_commit_pending_request_total` | guage | Number of pending commit requests. |
|
||||
| `argocd_commitserver_git_request_duration_seconds` | histogram | Git requests duration seconds. |
|
||||
| `argocd_commitserver_git_request_total` | counter | Number of git requests performed by commit server |
|
||||
| `argocd_commitserver_commit_request_duration_seconds` | histogram | Commit requests duration seconds. |
|
||||
| `argocd_commitserver_userinfo_request_duration_seconds` | histogram | Userinfo requests duration seconds. |
|
||||
| `argocd_commitserver_commit_request_total` | counter | Number of commit requests performed by commit server |
|
||||
|
||||
## Prometheus Operator
|
||||
|
||||
If using Prometheus Operator, the following ServiceMonitor example manifests can be used.
|
||||
|
||||
@@ -27,6 +27,7 @@ argocd-application-controller [flags]
|
||||
--client-certificate string Path to a client certificate file for TLS
|
||||
--client-key string Path to a client key file for TLS
|
||||
--cluster string The name of the kubeconfig cluster to use
|
||||
--commit-server string Commit server address. (default "argocd-commit-server:8086")
|
||||
--context string The name of the kubeconfig context to use
|
||||
--default-cache-expiration duration Cache expiration default (default 24h0m0s)
|
||||
--disable-compression If true, opt-out of response compression for all requests to the server
|
||||
@@ -34,6 +35,7 @@ argocd-application-controller [flags]
|
||||
--enable-k8s-event none Enable ArgoCD to use k8s event. For disabling all events, set the value as none. (e.g --enable-k8s-event=none), For enabling specific events, set the value as `event reason`. (e.g --enable-k8s-event=StatusRefreshed,ResourceCreated) (default [all])
|
||||
--gloglevel int Set the glog logging level
|
||||
-h, --help help for argocd-application-controller
|
||||
--hydrator-enabled Feature flag to enable Hydrator. Default ("false")
|
||||
--ignore-normalizer-jq-execution-timeout-seconds duration Set ignore normalizer JQ execution timeout
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--kubeconfig string Path to a kube config. Only required if out-of-cluster
|
||||
|
||||
@@ -45,6 +45,9 @@ argocd admin app generate-spec APPNAME [flags]
|
||||
--directory-exclude string Set glob expression used to exclude files from application source path
|
||||
--directory-include string Set glob expression used to include files from application source path
|
||||
--directory-recurse Recurse directory
|
||||
--dry-source-path string Path in repository to the app directory for the dry source
|
||||
--dry-source-repo string Repository URL of the app dry source
|
||||
--dry-source-revision string Revision of the app dry source
|
||||
--env string Application environment to monitor
|
||||
-f, --file string Filename or URL to Kubernetes manifests for the app
|
||||
--helm-api-versions stringArray Helm api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster
|
||||
@@ -60,6 +63,7 @@ argocd admin app generate-spec APPNAME [flags]
|
||||
--helm-skip-tests Skip helm test manifests installation step
|
||||
--helm-version string Helm version
|
||||
-h, --help help for generate-spec
|
||||
--hydrate-to-branch string The branch to hydrate the app to
|
||||
--ignore-missing-value-files Ignore locally missing valueFiles when setting helm template --values
|
||||
-i, --inline If set then generated resource is written back to the file specified in --file flag
|
||||
--jsonnet-ext-var-code stringArray Jsonnet ext var
|
||||
@@ -101,6 +105,8 @@ argocd admin app generate-spec APPNAME [flags]
|
||||
--sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2)
|
||||
--sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s)
|
||||
--sync-retry-limit int Max number of allowed sync retries
|
||||
--sync-source-branch string The branch from which the app will sync
|
||||
--sync-source-path string The path in the repository from which the app will sync
|
||||
--validate Validation of repo and cluster (default true)
|
||||
--values stringArray Helm values file(s) to use
|
||||
--values-literal-file string Filename or URL to import as a literal Helm values block
|
||||
|
||||
@@ -28,6 +28,9 @@ argocd app add-source APPNAME [flags]
|
||||
--directory-exclude string Set glob expression used to exclude files from application source path
|
||||
--directory-include string Set glob expression used to include files from application source path
|
||||
--directory-recurse Recurse directory
|
||||
--dry-source-path string Path in repository to the app directory for the dry source
|
||||
--dry-source-repo string Repository URL of the app dry source
|
||||
--dry-source-revision string Revision of the app dry source
|
||||
--env string Application environment to monitor
|
||||
--helm-api-versions stringArray Helm api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster
|
||||
--helm-chart string Helm Chart name
|
||||
@@ -42,6 +45,7 @@ argocd app add-source APPNAME [flags]
|
||||
--helm-skip-tests Skip helm test manifests installation step
|
||||
--helm-version string Helm version
|
||||
-h, --help help for add-source
|
||||
--hydrate-to-branch string The branch to hydrate the app to
|
||||
--ignore-missing-value-files Ignore locally missing valueFiles when setting helm template --values
|
||||
--jsonnet-ext-var-code stringArray Jsonnet ext var
|
||||
--jsonnet-ext-var-str stringArray Jsonnet string ext var
|
||||
@@ -78,6 +82,8 @@ argocd app add-source APPNAME [flags]
|
||||
--sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2)
|
||||
--sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s)
|
||||
--sync-retry-limit int Max number of allowed sync retries
|
||||
--sync-source-branch string The branch from which the app will sync
|
||||
--sync-source-path string The path in the repository from which the app will sync
|
||||
--validate Validation of repo and cluster (default true)
|
||||
--values stringArray Helm values file(s) to use
|
||||
--values-literal-file string Filename or URL to import as a literal Helm values block
|
||||
|
||||
6
docs/user-guide/commands/argocd_app_create.md
generated
6
docs/user-guide/commands/argocd_app_create.md
generated
@@ -47,6 +47,9 @@ argocd app create APPNAME [flags]
|
||||
--directory-exclude string Set glob expression used to exclude files from application source path
|
||||
--directory-include string Set glob expression used to include files from application source path
|
||||
--directory-recurse Recurse directory
|
||||
--dry-source-path string Path in repository to the app directory for the dry source
|
||||
--dry-source-repo string Repository URL of the app dry source
|
||||
--dry-source-revision string Revision of the app dry source
|
||||
--env string Application environment to monitor
|
||||
-f, --file string Filename or URL to Kubernetes manifests for the app
|
||||
--helm-api-versions stringArray Helm api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster
|
||||
@@ -62,6 +65,7 @@ argocd app create APPNAME [flags]
|
||||
--helm-skip-tests Skip helm test manifests installation step
|
||||
--helm-version string Helm version
|
||||
-h, --help help for create
|
||||
--hydrate-to-branch string The branch to hydrate the app to
|
||||
--ignore-missing-value-files Ignore locally missing valueFiles when setting helm template --values
|
||||
--jsonnet-ext-var-code stringArray Jsonnet ext var
|
||||
--jsonnet-ext-var-str stringArray Jsonnet string ext var
|
||||
@@ -101,6 +105,8 @@ argocd app create APPNAME [flags]
|
||||
--sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2)
|
||||
--sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s)
|
||||
--sync-retry-limit int Max number of allowed sync retries
|
||||
--sync-source-branch string The branch from which the app will sync
|
||||
--sync-source-path string The path in the repository from which the app will sync
|
||||
--upsert Allows to override application with the same name even if supplied application spec is different from existing spec
|
||||
--validate Validation of repo and cluster (default true)
|
||||
--values stringArray Helm values file(s) to use
|
||||
|
||||
6
docs/user-guide/commands/argocd_app_set.md
generated
6
docs/user-guide/commands/argocd_app_set.md
generated
@@ -40,6 +40,9 @@ argocd app set APPNAME [flags]
|
||||
--directory-exclude string Set glob expression used to exclude files from application source path
|
||||
--directory-include string Set glob expression used to include files from application source path
|
||||
--directory-recurse Recurse directory
|
||||
--dry-source-path string Path in repository to the app directory for the dry source
|
||||
--dry-source-repo string Repository URL of the app dry source
|
||||
--dry-source-revision string Revision of the app dry source
|
||||
--env string Application environment to monitor
|
||||
--helm-api-versions stringArray Helm api-versions (in format [group/]version/kind) to use when running helm template (Can be repeated to set several values: --helm-api-versions traefik.io/v1alpha1/TLSOption --helm-api-versions v1/Service). If not set, use the api-versions from the destination cluster
|
||||
--helm-chart string Helm Chart name
|
||||
@@ -54,6 +57,7 @@ argocd app set APPNAME [flags]
|
||||
--helm-skip-tests Skip helm test manifests installation step
|
||||
--helm-version string Helm version
|
||||
-h, --help help for set
|
||||
--hydrate-to-branch string The branch to hydrate the app to
|
||||
--ignore-missing-value-files Ignore locally missing valueFiles when setting helm template --values
|
||||
--jsonnet-ext-var-code stringArray Jsonnet ext var
|
||||
--jsonnet-ext-var-str stringArray Jsonnet string ext var
|
||||
@@ -91,6 +95,8 @@ argocd app set APPNAME [flags]
|
||||
--sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2)
|
||||
--sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s)
|
||||
--sync-retry-limit int Max number of allowed sync retries
|
||||
--sync-source-branch string The branch from which the app will sync
|
||||
--sync-source-path string The path in the repository from which the app will sync
|
||||
--validate Validation of repo and cluster (default true)
|
||||
--values stringArray Helm values file(s) to use
|
||||
--values-literal-file string Filename or URL to import as a literal Helm values block
|
||||
|
||||
1
docs/user-guide/commands/argocd_app_wait.md
generated
1
docs/user-guide/commands/argocd_app_wait.md
generated
@@ -43,6 +43,7 @@ argocd app wait [APPNAME.. | -l selector] [flags]
|
||||
--delete Wait for delete
|
||||
--health Wait for health
|
||||
-h, --help help for wait
|
||||
--hydrated Wait for hydration operations
|
||||
--operation Wait for pending operations
|
||||
-o, --output string Output format. One of: json|yaml|wide|tree|tree=detailed (default "wide")
|
||||
--resource stringArray Sync only specific resources as GROUP:KIND:NAME or !GROUP:KIND:NAME. Fields may be blank and '*' can be used. This option may be specified repeatedly
|
||||
|
||||
191
docs/user-guide/source-hydrator.md
Normal file
191
docs/user-guide/source-hydrator.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Source Hydrator
|
||||
|
||||
**Current feature state**: Alpha
|
||||
|
||||
Tools like Helm and Kustomize allow users to express their Kubernetes manifests in a more concise and reusable way
|
||||
(keeping it DRY - Don't Repeat Yourself). However, these tools can obscure the actual Kubernetes manifests that are
|
||||
applied to the cluster.
|
||||
|
||||
The "rendered manifest pattern" is a way to push the hydrated manifests to git before syncing them to the cluster. This
|
||||
allows users to see the actual Kubernetes manifests that are applied to the cluster.
|
||||
|
||||
The source hydrator is a feature of Argo CD that allows users to push the hydrated manifests to git before syncing them
|
||||
to the cluster.
|
||||
|
||||
## Enabling the Source Hydrator
|
||||
|
||||
The source hydrator is disabled by default.
|
||||
|
||||
To enable the source hydrator, you need to enable the "commit server" component and set the `hydrator.enabled` field in
|
||||
argocd-cmd-params-cm ConfigMap to `"true"`.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cmd-params-cm
|
||||
namespace: argocd
|
||||
data:
|
||||
hydrator.enabled: "true"
|
||||
```
|
||||
|
||||
!!! important
|
||||
After updating the ConfigMap, you must restart the Argo CD controller for the changes to take effect.
|
||||
|
||||
If you are using one of the `*-install.yaml` manifests to install Argo CD, you can use the
|
||||
`*-install-with-hydrator.yaml` version of that file instead.
|
||||
|
||||
For example,
|
||||
|
||||
```
|
||||
Without hydrator: https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
With hydrator: https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install-with-hydrator.yaml
|
||||
```
|
||||
|
||||
!!! important
|
||||
The `*-with-hydrator-install.yaml` manifests will eventually be removed when the source hydrator is either enabled
|
||||
by default or removed. The upgrade guide will note if the `install-with-hydrator.yaml` manifests are no longer
|
||||
available.
|
||||
|
||||
## Using the Source Hydrator
|
||||
|
||||
To use the source hydrator, you must first install a push secret.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: my-push-secret
|
||||
namespace: argocd
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: repository-write
|
||||
type: Opaque
|
||||
stringData:
|
||||
url: "https://github.com"
|
||||
type: "git"
|
||||
githubAppID: "<your app ID here>"
|
||||
githubAppInstallationID: "<your installation ID here>"
|
||||
githubAppPrivateKey: |
|
||||
<your private key here>
|
||||
```
|
||||
For now, the source hydrator only supports GitHub Apps. To use the source hydrator, you must first
|
||||
[create a GitHub App](https://github.com/settings/apps/new) with read/write permissions and install it in the repository
|
||||
you want to use it in. After you install the app, the installation ID will appear in the URL. The private key is
|
||||
generated when you create the GitHub App.
|
||||
Once your push secret is installed, set the `spec.sourceHydrator` field of the Application. For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
sourceHydrator:
|
||||
drySource:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps
|
||||
path: helm-guestbook
|
||||
targetRevision: HEAD
|
||||
syncSource:
|
||||
targetBranch: environments/dev
|
||||
path: helm-guestbook
|
||||
```
|
||||
|
||||
In this example, the hydrated manifests will be pushed to the `environments/dev` branch of the `argocd-example-apps`
|
||||
repository.
|
||||
|
||||
!!! important "Project-Scoped Repositories"
|
||||
|
||||
Repository Secrets may contain a `project` field, making the secret only usable by Applications in that project.
|
||||
The source hydrator only supports project-scoped repositories if all Applications writing to the same repository and
|
||||
branch are in the same project. If Applications in different projects write to the same repository and branch, the
|
||||
source hydrator will not be able to use a project-scoped repository secret and will require a global repository
|
||||
secret. This behavior may change in the future.
|
||||
|
||||
If there are multiple repository-write Secrets available for a repo, the source hydrator will non-deterministically
|
||||
select one of the matching Secrets and log a warning saying "Found multiple credentials for repoURL".
|
||||
|
||||
## Pushing to a "Staging" Branch
|
||||
|
||||
The source hydrator can be used to push hydrated manifests to a "staging" branch instead of the `syncSource` branch.
|
||||
This provides a way to prevent the hydrated manifests from being applied to the cluster until some prerequisite
|
||||
conditions are met (in effect providing a way to handle environment promotion via Pull Requests).
|
||||
|
||||
To use the source hydrator to push to a "staging" branch, set the `spec.sourceHydrator.hydrateTo` field of the
|
||||
Application. For example:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
project: my-project
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: default
|
||||
sourceHydrator:
|
||||
drySource:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps
|
||||
path: helm-guestbook
|
||||
targetRevision: HEAD
|
||||
syncSource:
|
||||
targetBranch: environments/dev
|
||||
path: helm-guestbook
|
||||
hydrateTo:
|
||||
targetBranch: environments/dev-next
|
||||
```
|
||||
|
||||
In this example, the hydrated manifests will be pushed to the `environments/dev-next` branch, and Argo CD will not sync
|
||||
the changes until something moves them to the `environments/dev` branch.
|
||||
|
||||
You could use a CI action to move the hydrated manifests from the `hydrateTo` branch to the `syncSource` branch. To
|
||||
introduce a gating mechanism, you could require a Pull Request to be opened to merge the changes from the `hydrateTo`
|
||||
branch to the `syncSource` branch.
|
||||
|
||||
Argo CD will only push changes to the `hydrateTo` branch, it will not create a PR or otherwise facilitate moving those
|
||||
changes to the `syncSource` branch. You will need to use your own tooling to move the changes from the `hydrateTo`
|
||||
branch to the `syncSource` branch.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Project-Scoped Push Secrets
|
||||
|
||||
If all the Applications for a given destination repo/branch are under the same project, then the hydrator will use any
|
||||
available project-scoped push secrets. If two Applications for a given repo/branch are in different projects, then the
|
||||
hydrator will not be able to use a project-scoped push secret and will require a global push secret.
|
||||
|
||||
### Credential Templates
|
||||
|
||||
Credential templates allow a single credential to be used for multiple repositories. The source hydrator does not
|
||||
currently support credential templates. You will need a separate credential for each repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Handle Secrets on the Destination Cluster
|
||||
|
||||
Do not use the source hydrator with any tool that injects secrets into your manifests as part of the hydration process
|
||||
(for example, Helm with SOPS or the Argo CD Vault Plugin). These secrets would be committed to git. Instead, use a
|
||||
secrets operator that populates the secret values on the destination cluster.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Make Hydration Deterministic
|
||||
|
||||
The source hydrator should be deterministic. For a given dry source commit, the hydrator should always produce the same
|
||||
hydrated manifests. This means that the hydrator should not rely on external state or configuration that is not stored
|
||||
in git.
|
||||
|
||||
Examples of non-deterministic hydration:
|
||||
|
||||
* A Helm chart using unpinned dependencies
|
||||
* A Helm chart is using a non-deterministic template function such as `randAlphaNum` or `lookup`
|
||||
* [Config Management Plugins](../operator-manual/config-management-plugins.md) which retrieve non-git state, such as secrets
|
||||
* Kustomize manifests referencing unpinned remote bases
|
||||
|
||||
### Enable Branch Protection
|
||||
|
||||
Argo CD should be the only thing pushing hydrated manifests to the hydrated branches. To prevent other tools or users
|
||||
from pushing to the hydrated branches, enable branch protection in your SCM.
|
||||
|
||||
It is best practice to prefix the hydrated branches with a common prefix, such as `environment/`. This makes it easier
|
||||
to configure branch protection rules on the destination repository.
|
||||
3
go.mod
3
go.mod
@@ -43,7 +43,7 @@ require (
|
||||
github.com/golang/protobuf v1.5.4
|
||||
github.com/google/btree v1.1.3
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-github/v63 v63.0.0
|
||||
github.com/google/go-github/v66 v66.0.0
|
||||
github.com/google/go-jsonnet v0.20.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -139,7 +139,6 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-github/v66 v66.0.0 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -437,8 +437,6 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg=
|
||||
github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg=
|
||||
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
|
||||
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
|
||||
github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M=
|
||||
github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4=
|
||||
github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g=
|
||||
|
||||
@@ -95,7 +95,7 @@ MOD_ROOT=${GOPATH}/pkg/mod
|
||||
grpc_gateway_version=$(go list -m github.com/grpc-ecosystem/grpc-gateway | awk '{print $NF}' | head -1)
|
||||
GOOGLE_PROTO_API_PATH=${MOD_ROOT}/github.com/grpc-ecosystem/grpc-gateway@${grpc_gateway_version}/third_party/googleapis
|
||||
GOGO_PROTOBUF_PATH=${PROJECT_ROOT}/vendor/github.com/gogo/protobuf
|
||||
PROTO_FILES=$(find "$PROJECT_ROOT" \( -name "*.proto" -and -path '*/server/*' -or -path '*/reposerver/*' -and -name "*.proto" -or -path '*/cmpserver/*' -and -name "*.proto" \) | sort)
|
||||
PROTO_FILES=$(find "$PROJECT_ROOT" \( -name "*.proto" -and -path '*/server/*' -or -path '*/reposerver/*' -and -name "*.proto" -or -path '*/cmpserver/*' -and -name "*.proto" -or -path '*/commitserver/*' -and -name "*.proto" -or -path '*/util/askpass/*' -and -name "*.proto" \) | sort)
|
||||
for i in ${PROTO_FILES}; do
|
||||
protoc \
|
||||
-I"${PROJECT_ROOT}" \
|
||||
@@ -110,6 +110,9 @@ for i in ${PROTO_FILES}; do
|
||||
"$i"
|
||||
done
|
||||
|
||||
# This file is generated but should not be checked in.
|
||||
rm util/askpass/askpass.swagger.json
|
||||
|
||||
[ -L "${GOPATH_PROJECT_ROOT}" ] && rm -rf "${GOPATH_PROJECT_ROOT}"
|
||||
[ -L ./v2 ] && rm -rf v2
|
||||
|
||||
@@ -162,3 +165,4 @@ clean_swagger server
|
||||
clean_swagger reposerver
|
||||
clean_swagger controller
|
||||
clean_swagger cmpserver
|
||||
clean_swagger commitserver
|
||||
|
||||
@@ -49,3 +49,21 @@ $KUSTOMIZE build "${SRCROOT}/manifests/ha/namespace-install" >> "${SRCROOT}/mani
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/core-install.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/core-install" >> "${SRCROOT}/manifests/core-install.yaml"
|
||||
|
||||
# Copies enabling manifest hydrator. These can be removed once the manifest hydrator is either removed or enabled by
|
||||
# default.
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/install-with-hydrator.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/cluster-install-with-hydrator" >> "${SRCROOT}/manifests/install-with-hydrator.yaml"
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/namespace-install-with-hydrator.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/namespace-install-with-hydrator" >> "${SRCROOT}/manifests/namespace-install-with-hydrator.yaml"
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/install-with-hydrator.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/ha/cluster-install-with-hydrator" >> "${SRCROOT}/manifests/ha/install-with-hydrator.yaml"
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/ha/namespace-install-with-hydrator.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/ha/namespace-install-with-hydrator" >> "${SRCROOT}/manifests/ha/namespace-install-with-hydrator.yaml"
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/core-install-with-hydrator.yaml"
|
||||
$KUSTOMIZE build "${SRCROOT}/manifests/core-install-with-hydrator" >> "${SRCROOT}/manifests/core-install-with-hydrator.yaml"
|
||||
|
||||
@@ -223,6 +223,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: controller.diff.server.side
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: hydrator.enabled
|
||||
optional: true
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
|
||||
@@ -232,6 +232,12 @@ spec:
|
||||
name: argocd-cmd-params-cm
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: hydrator.enabled
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
app.kubernetes.io/part-of: argocd
|
||||
app.kubernetes.io/component: commit-server
|
||||
name: argocd-commit-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
spec:
|
||||
serviceAccountName: argocd-commit-server
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
- name: argocd-commit-server
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- /usr/local/bin/argocd-commit-server
|
||||
env:
|
||||
- name: ARGOCD_COMMIT_SERVER_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: commitserver.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_COMMIT_SERVER_METRICS_LISTEN_ADDRESS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: commitserver.metrics.listen.address
|
||||
optional: true
|
||||
- name: ARGOCD_COMMIT_SERVER_LOGFORMAT
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: commitserver.log.format
|
||||
optional: true
|
||||
- name: ARGOCD_COMMIT_SERVER_LOGLEVEL
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: argocd-cmd-params-cm
|
||||
key: commitserver.log.level
|
||||
optional: true
|
||||
ports:
|
||||
- containerPort: 8086
|
||||
- containerPort: 8087
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz?full=true
|
||||
port: 8087
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
failureThreshold: 3
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8087
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
volumeMounts:
|
||||
- name: ssh-known-hosts
|
||||
mountPath: /app/config/ssh
|
||||
- name: tls-certs
|
||||
mountPath: /app/config/tls
|
||||
- name: gpg-keys
|
||||
mountPath: /app/config/gpg/source
|
||||
- name: gpg-keyring
|
||||
mountPath: /app/config/gpg/keys
|
||||
# We need a writeable temp directory for the askpass socket file.
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
initContainers:
|
||||
- command:
|
||||
- /bin/cp
|
||||
- -n
|
||||
- /usr/local/bin/argocd
|
||||
- /var/run/argocd/argocd-cmp-server
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
name: copyutil
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
readOnlyRootFilesystem: true
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
volumeMounts:
|
||||
- mountPath: /var/run/argocd
|
||||
name: var-files
|
||||
volumes:
|
||||
- name: ssh-known-hosts
|
||||
configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
- name: tls-certs
|
||||
configMap:
|
||||
name: argocd-tls-certs-cm
|
||||
- name: gpg-keys
|
||||
configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
- name: gpg-keyring
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
- name: argocd-commit-server-tls
|
||||
secret:
|
||||
secretName: argocd-commit-server-tls
|
||||
optional: true
|
||||
items:
|
||||
- key: tls.crt
|
||||
path: tls.crt
|
||||
- key: tls.key
|
||||
path: tls.key
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
- emptyDir: {}
|
||||
name: var-files
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
topologyKey: kubernetes.io/hostname
|
||||
- weight: 5
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/part-of: argocd
|
||||
topologyKey: kubernetes.io/hostname
|
||||
@@ -0,0 +1,22 @@
|
||||
kind: NetworkPolicy
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: argocd-commit-server-network-policy
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-application-controller
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8086
|
||||
- from:
|
||||
- namespaceSelector: { }
|
||||
ports:
|
||||
- port: 8087
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
app.kubernetes.io/part-of: argocd
|
||||
app.kubernetes.io/component: commit-server
|
||||
name: argocd-commit-server
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
app.kubernetes.io/part-of: argocd
|
||||
app.kubernetes.io/component: commit-server
|
||||
name: argocd-commit-server
|
||||
spec:
|
||||
ports:
|
||||
- name: server
|
||||
protocol: TCP
|
||||
port: 8086
|
||||
targetPort: 8086
|
||||
- name: metrics
|
||||
protocol: TCP
|
||||
port: 8087
|
||||
targetPort: 8087
|
||||
selector:
|
||||
app.kubernetes.io/name: argocd-commit-server
|
||||
8
manifests/base/commit-server/kustomization.yaml
Normal file
8
manifests/base/commit-server/kustomization.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- argocd-commit-server-sa.yaml
|
||||
- argocd-commit-server-deployment.yaml
|
||||
- argocd-commit-server-service.yaml
|
||||
- argocd-commit-server-network-policy.yaml
|
||||
12
manifests/cluster-install-with-hydrator/kustomization.yaml
Normal file
12
manifests/cluster-install-with-hydrator/kustomization.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
resources:
|
||||
- ../cluster-install
|
||||
- ../base/commit-server
|
||||
|
||||
patches:
|
||||
- target:
|
||||
kind: ConfigMap
|
||||
name: argocd-cmd-params-cm
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /data
|
||||
value: {"hydrator.enabled": "true"}
|
||||
25221
manifests/core-install-with-hydrator.yaml
Normal file
25221
manifests/core-install-with-hydrator.yaml
Normal file
File diff suppressed because it is too large
Load Diff
12
manifests/core-install-with-hydrator/kustomization.yaml
Normal file
12
manifests/core-install-with-hydrator/kustomization.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
resources:
|
||||
- ../core-install
|
||||
- ../base/commit-server
|
||||
|
||||
patches:
|
||||
- target:
|
||||
kind: ConfigMap
|
||||
name: argocd-cmd-params-cm
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /data
|
||||
value: {"hydrator.enabled": "true"}
|
||||
6
manifests/core-install.yaml
generated
6
manifests/core-install.yaml
generated
@@ -24864,6 +24864,12 @@ spec:
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: hydrator.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
resources:
|
||||
- ../cluster-install
|
||||
- ../../base/commit-server
|
||||
27546
manifests/ha/install-with-hydrator.yaml
Normal file
27546
manifests/ha/install-with-hydrator.yaml
Normal file
File diff suppressed because it is too large
Load Diff
3
manifests/ha/install-with-hydrator/kustomization.yaml
Normal file
3
manifests/ha/install-with-hydrator/kustomization.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
resources:
|
||||
- ../base
|
||||
- ../../base/commit-server
|
||||
6
manifests/ha/install.yaml
generated
6
manifests/ha/install.yaml
generated
@@ -26853,6 +26853,12 @@ spec:
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: hydrator.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
3776
manifests/ha/namespace-install-with-hydrator.yaml
Normal file
3776
manifests/ha/namespace-install-with-hydrator.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
resources:
|
||||
- ../namespace-install
|
||||
- ../../base/commit-server
|
||||
6
manifests/ha/namespace-install.yaml
generated
6
manifests/ha/namespace-install.yaml
generated
@@ -3083,6 +3083,12 @@ spec:
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: hydrator.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
26340
manifests/install-with-hydrator.yaml
Normal file
26340
manifests/install-with-hydrator.yaml
Normal file
File diff suppressed because it is too large
Load Diff
6
manifests/install.yaml
generated
6
manifests/install.yaml
generated
@@ -25923,6 +25923,12 @@ spec:
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: hydrator.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
2570
manifests/namespace-install-with-hydrator.yaml
Normal file
2570
manifests/namespace-install-with-hydrator.yaml
Normal file
File diff suppressed because it is too large
Load Diff
12
manifests/namespace-install-with-hydrator/kustomization.yaml
Normal file
12
manifests/namespace-install-with-hydrator/kustomization.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
resources:
|
||||
- ../namespace-install
|
||||
- ../base/commit-server
|
||||
|
||||
patches:
|
||||
- target:
|
||||
kind: ConfigMap
|
||||
name: argocd-cmd-params-cm
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /data
|
||||
value: {"hydrator.enabled": "true"}
|
||||
6
manifests/namespace-install.yaml
generated
6
manifests/namespace-install.yaml
generated
@@ -2153,6 +2153,12 @@ spec:
|
||||
key: controller.ignore.normalizer.jq.timeout
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: ARGOCD_HYDRATOR_ENABLED
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: hydrator.enabled
|
||||
name: argocd-cmd-params-cm
|
||||
optional: true
|
||||
- name: KUBECACHEDIR
|
||||
value: /tmp/kubecache
|
||||
image: quay.io/argoproj/argocd:latest
|
||||
|
||||
@@ -4,6 +4,8 @@ const (
|
||||
// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
|
||||
// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
|
||||
AnnotationKeyRefresh string = "argocd.argoproj.io/refresh"
|
||||
// AnnotationKeyHydrate is the annotation key which indicates that app needs to be hydrated. Removed by application controller after app is hydrated.
|
||||
AnnotationKeyHydrate string = "argocd.argoproj.io/hydrate"
|
||||
|
||||
// AnnotationKeyManifestGeneratePaths is an annotation that contains a list of semicolon-separated paths in the
|
||||
// manifests repository that affects the manifest generation. Paths might be either relative or absolute. The
|
||||
|
||||
@@ -219,6 +219,9 @@ func (a ApplicationSources) IsZero() bool {
|
||||
}
|
||||
|
||||
func (a *ApplicationSpec) GetSource() ApplicationSource {
|
||||
if a.SourceHydrator != nil {
|
||||
return a.SourceHydrator.GetSyncSource()
|
||||
}
|
||||
// if Application has multiple sources, return the first source in sources
|
||||
if a.HasMultipleSources() {
|
||||
return a.Sources[0]
|
||||
@@ -229,7 +232,26 @@ func (a *ApplicationSpec) GetSource() ApplicationSource {
|
||||
return ApplicationSource{}
|
||||
}
|
||||
|
||||
// GetHydrateToSource returns the hydrateTo source if it exists, otherwise returns the sync source.
|
||||
func (a *ApplicationSpec) GetHydrateToSource() ApplicationSource {
|
||||
if a.SourceHydrator != nil {
|
||||
targetRevision := a.SourceHydrator.SyncSource.TargetBranch
|
||||
if a.SourceHydrator.HydrateTo != nil {
|
||||
targetRevision = a.SourceHydrator.HydrateTo.TargetBranch
|
||||
}
|
||||
return ApplicationSource{
|
||||
RepoURL: a.SourceHydrator.DrySource.RepoURL,
|
||||
Path: a.SourceHydrator.SyncSource.Path,
|
||||
TargetRevision: targetRevision,
|
||||
}
|
||||
}
|
||||
return ApplicationSource{}
|
||||
}
|
||||
|
||||
func (a *ApplicationSpec) GetSources() ApplicationSources {
|
||||
if a.SourceHydrator != nil {
|
||||
return ApplicationSources{a.SourceHydrator.GetSyncSource()}
|
||||
}
|
||||
if a.HasMultipleSources() {
|
||||
return a.Sources
|
||||
}
|
||||
@@ -240,7 +262,7 @@ func (a *ApplicationSpec) GetSources() ApplicationSources {
|
||||
}
|
||||
|
||||
func (a *ApplicationSpec) HasMultipleSources() bool {
|
||||
return len(a.Sources) > 0
|
||||
return a.SourceHydrator == nil && len(a.Sources) > 0
|
||||
}
|
||||
|
||||
func (a *ApplicationSpec) GetSourcePtrByPosition(sourcePosition int) *ApplicationSource {
|
||||
@@ -249,6 +271,10 @@ func (a *ApplicationSpec) GetSourcePtrByPosition(sourcePosition int) *Applicatio
|
||||
}
|
||||
|
||||
func (a *ApplicationSpec) GetSourcePtrByIndex(sourceIndex int) *ApplicationSource {
|
||||
if a.SourceHydrator != nil {
|
||||
source := a.SourceHydrator.GetSyncSource()
|
||||
return &source
|
||||
}
|
||||
// if Application has multiple sources, return the first source in sources
|
||||
if a.HasMultipleSources() {
|
||||
if sourceIndex > 0 {
|
||||
@@ -366,6 +392,30 @@ type SourceHydrator struct {
|
||||
HydrateTo *HydrateTo `json:"hydrateTo,omitempty" protobuf:"bytes,3,opt,name=hydrateTo"`
|
||||
}
|
||||
|
||||
// GetSyncSource gets the source from which we should sync when a source hydrator is configured.
|
||||
func (s SourceHydrator) GetSyncSource() ApplicationSource {
|
||||
return ApplicationSource{
|
||||
// Pull the RepoURL from the dry source. The SyncSource's RepoURL is assumed to be the same.
|
||||
RepoURL: s.DrySource.RepoURL,
|
||||
Path: s.SyncSource.Path,
|
||||
TargetRevision: s.SyncSource.TargetBranch,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDrySource gets the dry source when a source hydrator is configured.
|
||||
func (s SourceHydrator) GetDrySource() ApplicationSource {
|
||||
return ApplicationSource{
|
||||
RepoURL: s.DrySource.RepoURL,
|
||||
Path: s.DrySource.Path,
|
||||
TargetRevision: s.DrySource.TargetRevision,
|
||||
}
|
||||
}
|
||||
|
||||
// DeepEquals returns true if the SourceHydrator is deeply equal to the given SourceHydrator.
|
||||
func (s SourceHydrator) DeepEquals(hydrator SourceHydrator) bool {
|
||||
return s.DrySource == hydrator.DrySource && s.SyncSource == hydrator.SyncSource && s.HydrateTo.DeepEquals(hydrator.HydrateTo)
|
||||
}
|
||||
|
||||
// DrySource specifies a location for dry "don't repeat yourself" manifest source information.
|
||||
type DrySource struct {
|
||||
// RepoURL is the URL to the git repository that contains the application manifests
|
||||
@@ -393,6 +443,19 @@ type HydrateTo struct {
|
||||
TargetBranch string `json:"targetBranch" protobuf:"bytes,1,name=targetBranch"`
|
||||
}
|
||||
|
||||
// DeepEquals returns true if the HydrateTo is deeply equal to the given HydrateTo.
|
||||
func (in *HydrateTo) DeepEquals(to *HydrateTo) bool {
|
||||
if in == nil {
|
||||
return to == nil
|
||||
}
|
||||
if to == nil {
|
||||
// We already know in is not nil.
|
||||
return false
|
||||
}
|
||||
// Compare de-referenced structs.
|
||||
return *in == *to
|
||||
}
|
||||
|
||||
// RefreshType specifies how to refresh the sources of a given application
|
||||
type RefreshType string
|
||||
|
||||
@@ -1105,6 +1168,16 @@ type SourceHydratorStatus struct {
|
||||
CurrentOperation *HydrateOperation `json:"currentOperation,omitempty" protobuf:"bytes,2,opt,name=currentOperation"`
|
||||
}
|
||||
|
||||
func (a *ApplicationStatus) FindResource(key kube.ResourceKey) (*ResourceStatus, bool) {
|
||||
for i := range a.Resources {
|
||||
res := a.Resources[i]
|
||||
if kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name) == key {
|
||||
return &res, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// HydrateOperation contains information about the most recent hydrate operation
|
||||
type HydrateOperation struct {
|
||||
// StartedAt indicates when the hydrate operation started
|
||||
@@ -1143,16 +1216,6 @@ const (
|
||||
HydrateOperationPhaseHydrated HydrateOperationPhase = "Hydrated"
|
||||
)
|
||||
|
||||
func (a *ApplicationStatus) FindResource(key kube.ResourceKey) (*ResourceStatus, bool) {
|
||||
for i := range a.Resources {
|
||||
res := a.Resources[i]
|
||||
if kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name) == key {
|
||||
return &res, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetRevisions will return the current revision associated with the Application.
|
||||
// If app has multisources, it will return all corresponding revisions preserving
|
||||
// order from the app.spec.sources. If app has only one source, it will return a
|
||||
@@ -2953,6 +3016,22 @@ func (app *Application) IsRefreshRequested() (RefreshType, bool) {
|
||||
return refreshType, true
|
||||
}
|
||||
|
||||
// IsHydrateRequested returns whether hydration has been requested for an application
|
||||
func (app *Application) IsHydrateRequested() bool {
|
||||
annotations := app.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
typeStr, ok := annotations[AnnotationKeyHydrate]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if typeStr == "normal" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (app *Application) HasPostDeleteFinalizer(stage ...string) bool {
|
||||
return getFinalizerIndex(app.ObjectMeta, strings.Join(append([]string{PostDeleteFinalizerName}, stage...), "/")) > -1
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ func GetDefaultAppRateLimiterConfig() *AppControllerRateLimiterConfig {
|
||||
|
||||
// NewCustomAppControllerRateLimiter is a constructor for the rate limiter for a workqueue used by app controller. It has
|
||||
// both overall and per-item rate limiting. The overall is a token bucket and the per-item is exponential(with auto resets)
|
||||
func NewCustomAppControllerRateLimiter(cfg *AppControllerRateLimiterConfig) workqueue.TypedRateLimiter[string] {
|
||||
return workqueue.NewTypedMaxOfRateLimiter[string](
|
||||
NewItemExponentialRateLimiterWithAutoReset(cfg.BaseDelay, cfg.MaxDelay, cfg.FailureCoolDown, cfg.BackoffFactor),
|
||||
&workqueue.TypedBucketRateLimiter[string]{Limiter: rate.NewLimiter(rate.Limit(cfg.BucketQPS), int(cfg.BucketSize))},
|
||||
func NewCustomAppControllerRateLimiter[T comparable](cfg *AppControllerRateLimiterConfig) workqueue.TypedRateLimiter[T] {
|
||||
return workqueue.NewTypedMaxOfRateLimiter[T](
|
||||
NewItemExponentialRateLimiterWithAutoReset[T](cfg.BaseDelay, cfg.MaxDelay, cfg.FailureCoolDown, cfg.BackoffFactor),
|
||||
&workqueue.TypedBucketRateLimiter[T]{Limiter: rate.NewLimiter(rate.Limit(cfg.BucketQPS), int(cfg.BucketSize))},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ type failureData struct {
|
||||
|
||||
// ItemExponentialRateLimiterWithAutoReset does a simple baseDelay*2^<num-failures> limit
|
||||
// dealing with max failures and expiration/resets are up dependent on the cooldown period
|
||||
type ItemExponentialRateLimiterWithAutoReset struct {
|
||||
type ItemExponentialRateLimiterWithAutoReset[T comparable] struct {
|
||||
failuresLock sync.Mutex
|
||||
failures map[interface{}]failureData
|
||||
|
||||
@@ -59,10 +59,10 @@ type ItemExponentialRateLimiterWithAutoReset struct {
|
||||
backoffFactor float64
|
||||
}
|
||||
|
||||
var _ workqueue.TypedRateLimiter[string] = &ItemExponentialRateLimiterWithAutoReset{}
|
||||
var _ workqueue.TypedRateLimiter[string] = &ItemExponentialRateLimiterWithAutoReset[string]{}
|
||||
|
||||
func NewItemExponentialRateLimiterWithAutoReset(baseDelay, maxDelay, failureCoolDown time.Duration, backoffFactor float64) workqueue.TypedRateLimiter[string] {
|
||||
return &ItemExponentialRateLimiterWithAutoReset{
|
||||
func NewItemExponentialRateLimiterWithAutoReset[T comparable](baseDelay, maxDelay, failureCoolDown time.Duration, backoffFactor float64) workqueue.TypedRateLimiter[T] {
|
||||
return &ItemExponentialRateLimiterWithAutoReset[T]{
|
||||
failures: map[interface{}]failureData{},
|
||||
baseDelay: baseDelay,
|
||||
maxDelay: maxDelay,
|
||||
@@ -71,7 +71,7 @@ func NewItemExponentialRateLimiterWithAutoReset(baseDelay, maxDelay, failureCool
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset) When(item string) time.Duration {
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset[T]) When(item T) time.Duration {
|
||||
r.failuresLock.Lock()
|
||||
defer r.failuresLock.Unlock()
|
||||
|
||||
@@ -109,14 +109,14 @@ func (r *ItemExponentialRateLimiterWithAutoReset) When(item string) time.Duratio
|
||||
return calculated
|
||||
}
|
||||
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset) NumRequeues(item string) int {
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset[T]) NumRequeues(item T) int {
|
||||
r.failuresLock.Lock()
|
||||
defer r.failuresLock.Unlock()
|
||||
|
||||
return r.failures[item].failures
|
||||
}
|
||||
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset) Forget(item string) {
|
||||
func (r *ItemExponentialRateLimiterWithAutoReset[T]) Forget(item T) {
|
||||
r.failuresLock.Lock()
|
||||
defer r.failuresLock.Unlock()
|
||||
|
||||
|
||||
@@ -1138,6 +1138,9 @@ func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclie
|
||||
if appHelm.ReleaseName != "" {
|
||||
templateOpts.Name = appHelm.ReleaseName
|
||||
}
|
||||
if appHelm.Namespace != "" {
|
||||
templateOpts.Namespace = appHelm.Namespace
|
||||
}
|
||||
|
||||
resolvedValueFiles, err := getResolvedValueFiles(appPath, repoRoot, env, q.GetValuesFileSchemes(), appHelm.ValueFiles, q.RefSources, gitRepoPaths, appHelm.IgnoreMissingValueFiles)
|
||||
if err != nil {
|
||||
@@ -2529,7 +2532,7 @@ func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bo
|
||||
}
|
||||
}
|
||||
|
||||
err = gitClient.Checkout(revision, submoduleEnabled)
|
||||
_, err = gitClient.Checkout(revision, submoduleEnabled)
|
||||
if err != nil {
|
||||
// When fetching with no revision, only refs/heads/* and refs/remotes/origin/* are fetched. If checkout fails
|
||||
// for the given revision, try explicitly fetching it.
|
||||
@@ -2541,7 +2544,7 @@ func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bo
|
||||
return status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
|
||||
}
|
||||
|
||||
err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
|
||||
_, err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *git
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return(mock.Anything, nil)
|
||||
gitClient.On("CommitSHA").Return(mock.Anything, nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
@@ -188,7 +188,7 @@ func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", revision).Return(revision, revisionErr)
|
||||
gitClient.On("CommitSHA").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
@@ -3071,7 +3071,7 @@ func TestCheckoutRevisionPresentSkipFetch(t *testing.T) {
|
||||
gitClient := &gitmocks.Client{}
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", revision).Return(true)
|
||||
gitClient.On("Checkout", revision, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", revision, mock.Anything).Return("", nil)
|
||||
|
||||
err := checkoutRevision(gitClient, revision, false)
|
||||
require.NoError(t, err)
|
||||
@@ -3084,7 +3084,7 @@ func TestCheckoutRevisionNotPresentCallFetch(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", revision).Return(false)
|
||||
gitClient.On("Fetch", "").Return(nil)
|
||||
gitClient.On("Checkout", revision, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", revision, mock.Anything).Return("", nil)
|
||||
|
||||
err := checkoutRevision(gitClient, revision, false)
|
||||
require.NoError(t, err)
|
||||
@@ -3410,7 +3410,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
gitClient.On("Root").Return(root)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3427,7 +3427,7 @@ func TestErrorGetGitDirectories(t *testing.T) {
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "ErrorVerifyCommit", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
gitClient.On("VerifyCommitSignature", mock.Anything).Return("", fmt.Errorf("revision %s is not signed", "sadfsadf"))
|
||||
gitClient.On("Root").Return(root)
|
||||
@@ -3464,7 +3464,7 @@ func TestGetGitDirectories(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
paths.On("GetPath", mock.Anything).Return(root, nil)
|
||||
@@ -3497,7 +3497,7 @@ func TestGetGitDirectoriesWithHiddenDirSupported(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
paths.On("GetPath", mock.Anything).Return(root, nil)
|
||||
@@ -3552,7 +3552,7 @@ func TestErrorGetGitFiles(t *testing.T) {
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
gitClient.On("Root").Return(root)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3591,7 +3591,7 @@ func TestGetGitFiles(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("Root").Return(root)
|
||||
gitClient.On("LsFiles", mock.Anything, mock.Anything).Once().Return(files, nil)
|
||||
@@ -3655,7 +3655,7 @@ func TestErrorUpdateRevisionForPaths(t *testing.T) {
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveRevision", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
gitClient.On("Root").Return(root)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3673,7 +3673,7 @@ func TestErrorUpdateRevisionForPaths(t *testing.T) {
|
||||
}, want: nil, wantErr: assert.Error},
|
||||
{name: "InvalidResolveSyncedRevision", fields: fields{service: func() *Service {
|
||||
s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error"))
|
||||
gitClient.On("Root").Return(root)
|
||||
@@ -3726,7 +3726,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
|
||||
}{
|
||||
{name: "NoPathAbort", fields: func() fields {
|
||||
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
}, ".")
|
||||
return fields{
|
||||
service: s,
|
||||
@@ -3741,7 +3741,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
|
||||
}, want: &apiclient.UpdateRevisionForPathsResponse{}, wantErr: assert.NoError},
|
||||
{name: "SameResolvedRevisionAbort", fields: func() fields {
|
||||
s, _, c := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) {
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3767,7 +3767,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3796,7 +3796,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
@@ -3834,7 +3834,7 @@ func TestUpdateRevisionForPaths(t *testing.T) {
|
||||
gitClient.On("Init").Return(nil)
|
||||
gitClient.On("IsRevisionPresent", mock.Anything).Return(false)
|
||||
gitClient.On("Fetch", mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil)
|
||||
gitClient.On("Checkout", mock.Anything, mock.Anything).Return("", nil)
|
||||
gitClient.On("LsRemote", "HEAD").Once().Return("632039659e542ed7de0c170a4fcc1c571b288fc0", nil)
|
||||
gitClient.On("LsRemote", "SYNCEDHEAD").Once().Return("1e67a504d03def3a6a1125d934cb511680f72555", nil)
|
||||
paths.On("GetPath", mock.Anything).Return(".", nil)
|
||||
|
||||
@@ -109,7 +109,7 @@ func fakeResolveRevisionResponseHelm() *apiclient.ResolveRevisionResponse {
|
||||
|
||||
func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient {
|
||||
mockRepoServiceClient := mocks.RepoServerServiceClient{}
|
||||
mockRepoServiceClient.On("ListApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil)
|
||||
mockRepoServiceClient.On("GetProcessableApps", mock.Anything, mock.Anything).Return(fakeAppList(), nil)
|
||||
mockRepoServiceClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{}, nil)
|
||||
mockRepoServiceClient.On("GetAppDetails", mock.Anything, mock.Anything).Return(&apiclient.RepoAppDetailsResponse{}, nil)
|
||||
mockRepoServiceClient.On("TestRepository", mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil)
|
||||
|
||||
@@ -555,7 +555,7 @@ func TestRepositoryServerListApps(t *testing.T) {
|
||||
AppProject: "default",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, resp.Items, 1)
|
||||
require.Len(t, resp.Items, 1)
|
||||
assert.Equal(t, "path/to/dir", resp.Items[0].Path)
|
||||
assert.Equal(t, "Kustomize", resp.Items[0].Type)
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run
|
||||
dex: sh -c "test $ARGOCD_IN_CI = true && exit 0; ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.41.1 serve /dex.yaml"
|
||||
redis: sh -c "/usr/local/bin/redis-server --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_BINARY_NAME=argocd-repo-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
commit-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_BINARY_NAME=argocd-commit-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_COMMITSERVER_PORT:-8086}"
|
||||
ui: sh -c "test $ARGOCD_IN_CI = true && exit 0; cd ui && ARGOCD_E2E_YARN_HOST=0.0.0.0 ${ARGOCD_E2E_YARN_CMD:-yarn} start"
|
||||
reaper: ./test/container/reaper.sh
|
||||
sshd: sudo sh -c "test $ARGOCD_E2E_TEST = true && /usr/sbin/sshd -p 2222 -D -e"
|
||||
|
||||
@@ -224,9 +224,14 @@ func (a *Actions) prepareCreateAppArgs(args []string) []string {
|
||||
a.context.t.Helper()
|
||||
args = append([]string{
|
||||
"app", "create", a.context.AppQualifiedName(),
|
||||
"--repo", fixture.RepoURL(a.context.repoURLType),
|
||||
}, args...)
|
||||
|
||||
if a.context.drySourceRevision != "" || a.context.drySourcePath != "" || a.context.syncSourcePath != "" || a.context.syncSourceBranch != "" || a.context.hydrateToBranch != "" {
|
||||
args = append(args, "--dry-source-repo", fixture.RepoURL(a.context.repoURLType))
|
||||
} else {
|
||||
args = append(args, "--repo", fixture.RepoURL(a.context.repoURLType))
|
||||
}
|
||||
|
||||
if a.context.destName != "" && a.context.isDestServerInferred && !slices.Contains(args, "--dest-server") {
|
||||
args = append(args, "--dest-name", a.context.destName)
|
||||
} else {
|
||||
@@ -236,6 +241,26 @@ func (a *Actions) prepareCreateAppArgs(args []string) []string {
|
||||
args = append(args, "--path", a.context.path)
|
||||
}
|
||||
|
||||
if a.context.drySourceRevision != "" {
|
||||
args = append(args, "--dry-source-revision", a.context.drySourceRevision)
|
||||
}
|
||||
|
||||
if a.context.drySourcePath != "" {
|
||||
args = append(args, "--dry-source-path", a.context.drySourcePath)
|
||||
}
|
||||
|
||||
if a.context.syncSourceBranch != "" {
|
||||
args = append(args, "--sync-source-branch", a.context.syncSourceBranch)
|
||||
}
|
||||
|
||||
if a.context.syncSourcePath != "" {
|
||||
args = append(args, "--sync-source-path", a.context.syncSourcePath)
|
||||
}
|
||||
|
||||
if a.context.hydrateToBranch != "" {
|
||||
args = append(args, "--hydrate-to-branch", a.context.hydrateToBranch)
|
||||
}
|
||||
|
||||
if a.context.chart != "" {
|
||||
args = append(args, "--helm-chart", a.context.chart)
|
||||
}
|
||||
|
||||
@@ -49,6 +49,11 @@ type Context struct {
|
||||
helmSkipTests bool
|
||||
trackingMethod v1alpha1.TrackingMethod
|
||||
sources []v1alpha1.ApplicationSource
|
||||
drySourceRevision string
|
||||
drySourcePath string
|
||||
syncSourceBranch string
|
||||
syncSourcePath string
|
||||
hydrateToBranch string
|
||||
}
|
||||
|
||||
type ContextArgs struct {
|
||||
@@ -243,6 +248,31 @@ func (c *Context) Path(path string) *Context {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) DrySourceRevision(revision string) *Context {
|
||||
c.drySourceRevision = revision
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) DrySourcePath(path string) *Context {
|
||||
c.drySourcePath = path
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) SyncSourceBranch(branch string) *Context {
|
||||
c.syncSourceBranch = branch
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) SyncSourcePath(path string) *Context {
|
||||
c.syncSourcePath = path
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) HydrateToBranch(branch string) *Context {
|
||||
c.hydrateToBranch = branch
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Context) Recurse() *Context {
|
||||
c.directoryRecurse = true
|
||||
return c
|
||||
|
||||
102
test/e2e/hydrator_test.go
Normal file
102
test/e2e/hydrator_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app"
|
||||
|
||||
. "github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
)
|
||||
|
||||
func TestSimpleHydrator(t *testing.T) {
|
||||
Given(t).
|
||||
DrySourcePath("guestbook").
|
||||
DrySourceRevision("HEAD").
|
||||
SyncSourcePath("guestbook").
|
||||
SyncSourceBranch("env/test").
|
||||
When().
|
||||
CreateApp().
|
||||
Refresh(RefreshTypeNormal).
|
||||
Wait("--hydrated").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced))
|
||||
}
|
||||
|
||||
func TestHydrateTo(t *testing.T) {
|
||||
Given(t).
|
||||
DrySourcePath("guestbook").
|
||||
DrySourceRevision("HEAD").
|
||||
SyncSourcePath("guestbook").
|
||||
SyncSourceBranch("env/test").
|
||||
HydrateToBranch("env/test-next").
|
||||
When().
|
||||
CreateApp().
|
||||
Refresh(RefreshTypeNormal).
|
||||
Wait("--hydrated").
|
||||
Then().
|
||||
Given().
|
||||
// Async so we don't fail immediately on the error
|
||||
Async(true).
|
||||
When().
|
||||
Sync().
|
||||
Wait("--operation").
|
||||
Then().
|
||||
// Fails because we hydrated to env/test-next but not to env/test.
|
||||
Expect(OperationPhaseIs(OperationError)).
|
||||
When().
|
||||
// Will now hydrate to the sync source branch.
|
||||
AppSet("--hydrate-to-branch", "").
|
||||
Refresh(RefreshTypeNormal).
|
||||
Wait("--hydrated").
|
||||
Sync().
|
||||
Wait("--operation").
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced))
|
||||
}
|
||||
|
||||
func TestAddingApp(t *testing.T) {
|
||||
// Make sure that if we add another app targeting the same sync branch, it hydrates correctly.
|
||||
Given(t).
|
||||
Name("test-adding-app-1").
|
||||
DrySourcePath("guestbook").
|
||||
DrySourceRevision("HEAD").
|
||||
SyncSourcePath("guestbook-1").
|
||||
SyncSourceBranch("env/test").
|
||||
When().
|
||||
CreateApp().
|
||||
Refresh(RefreshTypeNormal).
|
||||
Wait("--hydrated").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
Given().
|
||||
Name("test-adding-app-2").
|
||||
DrySourcePath("guestbook").
|
||||
DrySourceRevision("HEAD").
|
||||
SyncSourcePath("guestbook-2").
|
||||
SyncSourceBranch("env/test").
|
||||
When().
|
||||
CreateApp().
|
||||
Refresh(RefreshTypeNormal).
|
||||
Wait("--hydrated").
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
// Clean up the apps manually since we used custom names.
|
||||
When().
|
||||
Delete(true).
|
||||
Then().
|
||||
Expect(DoesNotExist()).
|
||||
Given().
|
||||
Name("test-adding-app-1").
|
||||
When().
|
||||
Delete(true).
|
||||
Then().
|
||||
Expect(DoesNotExist())
|
||||
}
|
||||
@@ -231,6 +231,7 @@ func RefreshApp(appIf v1alpha1.ApplicationInterface, name string, refreshType ar
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]string{
|
||||
argoappv1.AnnotationKeyRefresh: string(refreshType),
|
||||
argoappv1.AnnotationKeyHydrate: "normal",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -417,6 +418,12 @@ func validateRepo(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
// If using the source hydrator, check the dry source instead of the sync source, since the sync source branch may
|
||||
// not exist yet.
|
||||
if app.Spec.SourceHydrator != nil {
|
||||
sources = []argoappv1.ApplicationSource{app.Spec.SourceHydrator.GetDrySource()}
|
||||
}
|
||||
|
||||
refSources, err := GetRefSources(ctx, sources, app.Spec.Project, db.GetRepository, []string{}, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting ref sources: %w", err)
|
||||
@@ -555,11 +562,46 @@ func validateSourcePermissions(source argoappv1.ApplicationSource, hasMultipleSo
|
||||
return conditions
|
||||
}
|
||||
|
||||
func validateSourceHydrator(hydrator *argoappv1.SourceHydrator) []argoappv1.ApplicationCondition {
|
||||
var conditions []argoappv1.ApplicationCondition
|
||||
if hydrator.DrySource.RepoURL == "" {
|
||||
conditions = append(conditions, argoappv1.ApplicationCondition{
|
||||
Type: argoappv1.ApplicationConditionInvalidSpecError,
|
||||
Message: "spec.sourceHydrator.drySource.repoURL is required",
|
||||
})
|
||||
}
|
||||
if hydrator.SyncSource.TargetBranch == "" {
|
||||
conditions = append(conditions, argoappv1.ApplicationCondition{
|
||||
Type: argoappv1.ApplicationConditionInvalidSpecError,
|
||||
Message: "spec.sourceHydrator.syncSource.targetBranch is required",
|
||||
})
|
||||
}
|
||||
if hydrator.HydrateTo != nil && hydrator.HydrateTo.TargetBranch == "" {
|
||||
conditions = append(conditions, argoappv1.ApplicationCondition{
|
||||
Type: argoappv1.ApplicationConditionInvalidSpecError,
|
||||
Message: "when spec.sourceHydrator.hydrateTo is set, spec.sourceHydrator.hydrateTo.path is required",
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
// ValidatePermissions ensures that the referenced cluster has been added to Argo CD and the app source repo and destination namespace/cluster are permitted in app project
|
||||
func ValidatePermissions(ctx context.Context, spec *argoappv1.ApplicationSpec, proj *argoappv1.AppProject, db db.ArgoDB) ([]argoappv1.ApplicationCondition, error) {
|
||||
conditions := make([]argoappv1.ApplicationCondition, 0)
|
||||
|
||||
if spec.HasMultipleSources() {
|
||||
if spec.SourceHydrator != nil {
|
||||
condition := validateSourceHydrator(spec.SourceHydrator)
|
||||
if len(condition) > 0 {
|
||||
conditions = append(conditions, condition...)
|
||||
return conditions, nil
|
||||
}
|
||||
if !proj.IsSourcePermitted(spec.SourceHydrator.GetDrySource()) {
|
||||
conditions = append(conditions, argoappv1.ApplicationCondition{
|
||||
Type: argoappv1.ApplicationConditionInvalidSpecError,
|
||||
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.GetSource().RepoURL, spec.Project),
|
||||
})
|
||||
}
|
||||
} else if spec.HasMultipleSources() {
|
||||
for _, source := range spec.Sources {
|
||||
condition := validateSourcePermissions(source, spec.HasMultipleSources())
|
||||
if len(condition) > 0 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||
// source: reposerver/askpass/askpass.proto
|
||||
// source: util/askpass/askpass.proto
|
||||
|
||||
package askpass
|
||||
|
||||
@@ -37,7 +37,7 @@ func (m *CredentialsRequest) Reset() { *m = CredentialsRequest{} }
|
||||
func (m *CredentialsRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*CredentialsRequest) ProtoMessage() {}
|
||||
func (*CredentialsRequest) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_099f282cab154dba, []int{0}
|
||||
return fileDescriptor_1c7c1d31cf056104, []int{0}
|
||||
}
|
||||
func (m *CredentialsRequest) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -85,7 +85,7 @@ func (m *CredentialsResponse) Reset() { *m = CredentialsResponse{} }
|
||||
func (m *CredentialsResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*CredentialsResponse) ProtoMessage() {}
|
||||
func (*CredentialsResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_099f282cab154dba, []int{1}
|
||||
return fileDescriptor_1c7c1d31cf056104, []int{1}
|
||||
}
|
||||
func (m *CredentialsResponse) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
@@ -133,25 +133,25 @@ func init() {
|
||||
proto.RegisterType((*CredentialsResponse)(nil), "askpass.CredentialsResponse")
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("reposerver/askpass/askpass.proto", fileDescriptor_099f282cab154dba) }
|
||||
func init() { proto.RegisterFile("util/askpass/askpass.proto", fileDescriptor_1c7c1d31cf056104) }
|
||||
|
||||
var fileDescriptor_099f282cab154dba = []byte{
|
||||
// 231 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0x28, 0x4a, 0x2d, 0xc8,
|
||||
0x2f, 0x4e, 0x2d, 0x2a, 0x4b, 0x2d, 0xd2, 0x4f, 0x2c, 0xce, 0x2e, 0x48, 0x2c, 0x2e, 0x86, 0xd1,
|
||||
0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0x42, 0xec, 0x50, 0xae, 0x92, 0x16, 0x97, 0x90, 0x73, 0x51,
|
||||
0x6a, 0x4a, 0x6a, 0x5e, 0x49, 0x66, 0x62, 0x4e, 0x71, 0x50, 0x6a, 0x61, 0x69, 0x6a, 0x71, 0x89,
|
||||
0x90, 0x08, 0x17, 0x6b, 0x5e, 0x7e, 0x5e, 0x72, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10,
|
||||
0x84, 0xa3, 0xe4, 0xcb, 0x25, 0x8c, 0xa2, 0xb6, 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0x55, 0x48, 0x8a,
|
||||
0x8b, 0xa3, 0xb4, 0x38, 0xb5, 0x28, 0x2f, 0x31, 0x17, 0xa6, 0x1e, 0xce, 0x07, 0xc9, 0x81, 0xac,
|
||||
0x29, 0xcf, 0x2f, 0x4a, 0x91, 0x60, 0x82, 0xc8, 0xc1, 0xf8, 0x46, 0xf1, 0x5c, 0x7c, 0x8e, 0xc5,
|
||||
0xd9, 0x01, 0x89, 0xc5, 0xc5, 0xc1, 0xa9, 0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xbe, 0x5c, 0x7c,
|
||||
0xee, 0xa9, 0x25, 0x48, 0x76, 0x08, 0x49, 0xeb, 0xc1, 0xdc, 0x8d, 0xe9, 0x4a, 0x29, 0x19, 0xec,
|
||||
0x92, 0x10, 0x67, 0x29, 0x31, 0x38, 0xd9, 0x9f, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3,
|
||||
0x83, 0x47, 0x72, 0x8c, 0x51, 0x86, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9,
|
||||
0xfa, 0x89, 0x45, 0xe9, 0xf9, 0x05, 0x45, 0xf9, 0x59, 0x60, 0x86, 0x6e, 0x72, 0x8a, 0x7e, 0x99,
|
||||
0x91, 0x3e, 0x66, 0x98, 0x25, 0xb1, 0x81, 0x03, 0xcb, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x5a,
|
||||
0x1e, 0xa9, 0xaf, 0x50, 0x01, 0x00, 0x00,
|
||||
var fileDescriptor_1c7c1d31cf056104 = []byte{
|
||||
// 225 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2a, 0x2d, 0xc9, 0xcc,
|
||||
0xd1, 0x4f, 0x2c, 0xce, 0x2e, 0x48, 0x2c, 0x2e, 0x86, 0xd1, 0x7a, 0x05, 0x45, 0xf9, 0x25, 0xf9,
|
||||
0x42, 0xec, 0x50, 0xae, 0x92, 0x16, 0x97, 0x90, 0x73, 0x51, 0x6a, 0x4a, 0x6a, 0x5e, 0x49, 0x66,
|
||||
0x62, 0x4e, 0x71, 0x50, 0x6a, 0x61, 0x69, 0x6a, 0x71, 0x89, 0x90, 0x08, 0x17, 0x6b, 0x5e, 0x7e,
|
||||
0x5e, 0x72, 0xaa, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x67, 0x10, 0x84, 0xa3, 0xe4, 0xcb, 0x25, 0x8c,
|
||||
0xa2, 0xb6, 0xb8, 0x20, 0x3f, 0xaf, 0x38, 0x55, 0x48, 0x8a, 0x8b, 0xa3, 0xb4, 0x38, 0xb5, 0x28,
|
||||
0x2f, 0x31, 0x17, 0xa6, 0x1e, 0xce, 0x07, 0xc9, 0x81, 0xac, 0x29, 0xcf, 0x2f, 0x4a, 0x91, 0x60,
|
||||
0x82, 0xc8, 0xc1, 0xf8, 0x46, 0xf1, 0x5c, 0x7c, 0x8e, 0xc5, 0xd9, 0x01, 0x89, 0xc5, 0xc5, 0xc1,
|
||||
0xa9, 0x45, 0x65, 0x99, 0xc9, 0xa9, 0x42, 0xbe, 0x5c, 0x7c, 0xee, 0xa9, 0x25, 0x48, 0x76, 0x08,
|
||||
0x49, 0xeb, 0xc1, 0xdc, 0x8d, 0xe9, 0x4a, 0x29, 0x19, 0xec, 0x92, 0x10, 0x67, 0x29, 0x31, 0x38,
|
||||
0x59, 0x9e, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x51, 0xda,
|
||||
0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9, 0xf9, 0xb9, 0xfa, 0x89, 0x45, 0xe9, 0xf9, 0x05,
|
||||
0x45, 0xf9, 0x59, 0x60, 0x86, 0x6e, 0x72, 0x8a, 0x7e, 0x99, 0x91, 0x3e, 0x72, 0x68, 0x25, 0xb1,
|
||||
0x81, 0x83, 0xc9, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0xa8, 0xcc, 0x96, 0x87, 0x44, 0x01, 0x00,
|
||||
0x00,
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
@@ -231,7 +231,7 @@ var _AskPassService_serviceDesc = grpc.ServiceDesc{
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "reposerver/askpass/askpass.proto",
|
||||
Metadata: "util/askpass/askpass.proto",
|
||||
}
|
||||
|
||||
func (m *CredentialsRequest) Marshal() (dAtA []byte, err error) {
|
||||
@@ -1,5 +1,5 @@
|
||||
syntax = "proto3";
|
||||
option go_package = "github.com/argoproj/argo-cd/v2/reposerver/askpass";
|
||||
option go_package = "github.com/argoproj/argo-cd/v2/util/askpass";
|
||||
|
||||
package askpass;
|
||||
|
||||
@@ -11,6 +11,8 @@ const (
|
||||
ASKPASS_NONCE_ENV = "ARGOCD_GIT_ASKPASS_NONCE"
|
||||
// AKSPASS_SOCKET_PATH_ENV is the environment variable that is used to pass the socket path to the askpass script
|
||||
AKSPASS_SOCKET_PATH_ENV = "ARGOCD_ASK_PASS_SOCK"
|
||||
// CommitServerSocketPath is the path to the socket used by the commit server to communicate with the askpass server
|
||||
CommitServerSocketPath = "/tmp/commit-server-ask-pass.sock"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -61,6 +61,9 @@ type ArgoDB interface {
|
||||
// DeleteRepository deletes a repository from config
|
||||
DeleteRepository(ctx context.Context, name, project string) error
|
||||
|
||||
// GetWriteRepository returns a repository by URL with write credentials
|
||||
GetWriteRepository(ctx context.Context, url, project string) (*appv1.Repository, error)
|
||||
|
||||
// ListRepositoryCredentials list all repo credential sets URL patterns
|
||||
ListRepositoryCredentials(ctx context.Context) ([]string, error)
|
||||
// GetRepositoryCredentials gets repo credentials for given URL
|
||||
@@ -81,6 +84,9 @@ type ArgoDB interface {
|
||||
// GetAllHelmRepositoryCredentials gets all repo credentials
|
||||
GetAllHelmRepositoryCredentials(ctx context.Context) ([]*appv1.RepoCreds, error)
|
||||
|
||||
// GetWriteCredentials gets repo credentials specific to the hydrator for given URL
|
||||
GetWriteCredentials(ctx context.Context, repoURL string) (*appv1.Repository, error)
|
||||
|
||||
// ListHelmRepositories lists repositories
|
||||
ListHelmRepositories(ctx context.Context) ([]*appv1.Repository, error)
|
||||
|
||||
|
||||
60
util/db/mocks/ArgoDB.go
generated
60
util/db/mocks/ArgoDB.go
generated
@@ -475,6 +475,66 @@ func (_m *ArgoDB) GetRepositoryCredentials(ctx context.Context, name string) (*v
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetWriteCredentials provides a mock function with given fields: ctx, repoURL
|
||||
func (_m *ArgoDB) GetWriteCredentials(ctx context.Context, repoURL string) (*v1alpha1.Repository, error) {
|
||||
ret := _m.Called(ctx, repoURL)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetWriteCredentials")
|
||||
}
|
||||
|
||||
var r0 *v1alpha1.Repository
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (*v1alpha1.Repository, error)); ok {
|
||||
return rf(ctx, repoURL)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *v1alpha1.Repository); ok {
|
||||
r0 = rf(ctx, repoURL)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, repoURL)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetWriteRepository provides a mock function with given fields: ctx, url, project
|
||||
func (_m *ArgoDB) GetWriteRepository(ctx context.Context, url string, project string) (*v1alpha1.Repository, error) {
|
||||
ret := _m.Called(ctx, url, project)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetWriteRepository")
|
||||
}
|
||||
|
||||
var r0 *v1alpha1.Repository
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) (*v1alpha1.Repository, error)); ok {
|
||||
return rf(ctx, url, project)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) *v1alpha1.Repository); ok {
|
||||
r0 = rf(ctx, url, project)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.Repository)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, url, project)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ListClusters provides a mock function with given fields: ctx
|
||||
func (_m *ArgoDB) ListClusters(ctx context.Context) (*v1alpha1.ClusterList, error) {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user