Compare commits

...

16 Commits

Author SHA1 Message Date
Alexander Matyushentsev
3ca632a552 Update manifests to v1.0.0-rc2 2019-04-30 13:19:50 -07:00
Alexander Matyushentsev
cfe55357ac Rollout health checks/actions should support v0.2 and v0.2+ versions (#1543) 2019-04-30 13:18:15 -07:00
Alex Collins
0f6d768eca Fixes bug in normalizer (#1542) 2019-04-30 11:32:54 -07:00
Alexander Matyushentsev
75330da328 Ingress resource might get invalid ExternalURL (#1522) (#1523) 2019-04-30 11:14:18 -07:00
Alexander Matyushentsev
a1bcbab0e5 Issue 1476 - Avoid validating repository in application controller (#1535) 2019-04-30 11:10:06 -07:00
Alexander Matyushentsev
db9272032a Issue #1414 - Load target resource using K8S if conversion fails (#1527) 2019-04-30 11:10:02 -07:00
Alexander Matyushentsev
d6d6c655ff Issue #1476 - Add repo server grpc call timeout (#1528) 2019-04-30 11:09:58 -07:00
Alex Collins
58acc92790 Adds support for configuring repo creds at a domain/org level. Closes… (#1496) 2019-04-30 11:09:53 -07:00
Simon Behar
c3074c0977 Whitelisting of resources (#1509)
* Added whitelisting of resources
2019-04-30 11:09:26 -07:00
Simon Behar
af254f3047 Added ability to sync specific labels from the command line (#1501)
* Finished initial implementation

* Added tests and fix a few bugs
2019-04-30 11:09:16 -07:00
Alex Collins
c140976eeb Updates Makefile 2019-04-24 10:52:53 -07:00
Alex Collins
05c22d4ddc Updates VERSION 2019-04-24 10:51:50 -07:00
Alex Collins
3e08938a20 Updates VERSION 2019-04-24 10:49:30 -07:00
Alex Collins
5937bb574d Update manifests to v1.0.0-rc1 2019-04-24 10:48:24 -07:00
Alex Collins
ded55b26d1 Update manifests to v1.0.0-rc1 2019-04-24 10:46:23 -07:00
Alex Collins
d79ed65de0 Updated CHANGELOG.md 2019-04-24 10:35:04 -07:00
53 changed files with 1506 additions and 315 deletions

View File

@@ -22,17 +22,11 @@ Argo CD introduces Custom Resource Actions to allow users to provide their own L
* Remove deprecated componentParameterOverrides field #1372
### Changes since v0.12.1
#### New Features
+ Issue #357 - Expose application nodes networking information (#1333)
+ Support for customizable resource actions as Lua scripts #86
+ Surface Service/Ingress external IPs, hostname to application #908
+ Update argocd-util import/export to support proper backup and restore (#1328)
### Changes since v0.12.2
#### Enhancements
* `argocd app wait` should have `--resource` flag like sync #1206
* Adds support for `kustomize edit set image`. Closes #1275 (#1324)
* Allow wait to return on health or suspended (#1392)
* Application warning when a manifest is defined twice #1070
@@ -41,30 +35,32 @@ Argo CD introduces Custom Resource Actions to allow users to provide their own L
* Display number of errors on resource tab #1477
* Displays resources that are being deleted as "Progressing". Closes #1410 (#1426)
* Generate random name for grpc proxy unix socket file instead of time stamp (#1455)
* Issue #357 - Expose application nodes networking information (#1333)
* Issue #1404 - App controller unnecessary set namespace to cluster level resources (#1405)
* Nils health if the resource does not provide it. Closes #1383 (#1408)
* Perform health assessments on all resource nodes in the tree. Closes #1382 (#1422)
* Remove deprecated componentParameterOverrides field #1372
* Shows the health of the application. Closes #1433 (#1434)
* Surface Service/Ingress external IPs, hostname to application #908
* Surface pod status to tree view #1358
* Support for customizable resource actions as Lua scripts #86
* UI / API Errors Truncated, Time Out #1386
* UI Enhancement Proposals Quick Wins #1274
* Update argocd-util import/export to support proper backup and restore (#1328)
* Whitelisting repos/clusters in projects should consider repo/cluster permissions #1432
#### Bug Fixes
- "bind: address already in use" after switching to gRPC-Web #1451
- Annoying warning while using `--grpc-web` flag #1420
- Don't compare secrets in the CLI, since argo-cd doesn't have access to their data (#1459)
- Dropdown menu should not have sync item for unmanaged resources #1357
- Fixes goroutine leak. Closes #1381 (#1457)
- Improve input style #1217
- Issue #1389 - Fix null pointer exception in secret normalization function (#1428)
- Issue #1425 - Argo CD should not delete CRDs (#1428)
- Issue #1446 - Delete helm temp directories (#1449)
- Issue #908 - Surface Service/Ingress external IPs, hostname to application (#1347)
- kustomization fields are all mandatory #1504
- Resource node details is crashing if live resource is missing $1505
- Rollback UI is not showing correct ksonnet parameters in preview #1326
- See details of applications fails with "r.nodes is undefined" #1371
- UI fails to load custom actions is resource is not deployed #1502
- Unable to create app from private repo: x509: certificate signed by unknown authority #1171
## v0.12.2 (2019-04-22)

View File

@@ -148,6 +148,7 @@ test-e2e: cli
.PHONY: start-e2e
start-e2e: cli
killall goreman || true
kubectl create ns argocd-e2e || true
kubens argocd-e2e
kustomize build test/manifests/base | kubectl apply -f -
@@ -178,4 +179,4 @@ release-precheck: manifests
@if [ "$(GIT_TAG)" != "v`cat VERSION`" ]; then echo 'VERSION does not match git tag'; exit 1; fi
.PHONY: release
release: release-precheck precheckin image release-cli
release: release-precheck pre-commit image release-cli

View File

@@ -2,4 +2,4 @@ controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/a
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app"
repo-server: sh -c "FORCE_LOG_COLORS=1 go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379"
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.14.0 serve /dex.yaml"
redis: docker run --rm -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
redis: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save ""--appendonly no

View File

@@ -1 +1 @@
1.0.0
1.0.0-rc2

View File

@@ -37,14 +37,15 @@ const (
func newCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
appResyncPeriod int64
repoServerAddress string
statusProcessors int
operationProcessors int
logLevel string
glogLevel int
cacheSrc func() (*cache.Cache, error)
clientConfig clientcmd.ClientConfig
appResyncPeriod int64
repoServerAddress string
repoServerTimeoutSeconds int
statusProcessors int
operationProcessors int
logLevel string
glogLevel int
cacheSrc func() (*cache.Cache, error)
)
var command = cobra.Command{
Use: cliName,
@@ -65,7 +66,7 @@ func newCommand() *cobra.Command {
errors.CheckError(err)
resyncDuration := time.Duration(appResyncPeriod) * time.Second
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress)
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -98,6 +99,7 @@ func newCommand() *cobra.Command {
clientConfig = cli.AddKubectlFlagsToCmd(&command)
command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")

View File

@@ -57,7 +57,7 @@ func NewCommand() *cobra.Command {
kubeclientset := kubernetes.NewForConfigOrDie(config)
appclientset := appclientset.NewForConfigOrDie(config)
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress)
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress, 0)
argoCDOpts := server.ArgoCDServerOpts{
Insecure: insecure,

View File

@@ -957,6 +957,7 @@ func formatConditionsSummary(app argoappv1.Application) string {
const (
resourceFieldDelimiter = ":"
resourceFieldCount = 3
labelFieldDelimiter = "="
)
func parseSelectedResources(resources []string) []argoappv1.SyncOperationResource {
@@ -979,6 +980,21 @@ func parseSelectedResources(resources []string) []argoappv1.SyncOperationResourc
return selectedResources
}
func parseLabels(labels []string) (map[string]string, error) {
var selectedLabels map[string]string
if labels != nil {
selectedLabels = map[string]string{}
for _, r := range labels {
fields := strings.Split(r, labelFieldDelimiter)
if len(fields) != 2 {
return nil, fmt.Errorf("labels should have key%svalue, but instead got: %s", labelFieldDelimiter, r)
}
selectedLabels[fields[0]] = fields[1]
}
}
return selectedLabels, nil
}
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
@@ -1073,6 +1089,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
var (
revision string
resources []string
labels []string
prune bool
dryRun bool
timeout uint
@@ -1091,8 +1108,51 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
conn, appIf := acdClient.NewApplicationClientOrDie()
defer util.Close(conn)
selectedResources := parseSelectedResources(resources)
appName := args[0]
selectedLabels, parseErr := parseLabels(labels)
if parseErr != nil {
log.Fatal(parseErr)
}
if len(selectedLabels) > 0 {
ctx := context.Background()
if revision == "" {
revision = "HEAD"
}
q := application.ApplicationManifestQuery{
Name: &appName,
Revision: revision,
}
res, err := appIf.GetManifests(ctx, &q)
if err != nil {
log.Fatal(err)
}
for _, mfst := range res.Manifests {
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
errors.CheckError(err)
for key, selectedValue := range selectedLabels {
if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
gvk := obj.GroupVersionKind()
resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
}
}
}
// If labels are provided and none are found return error only if specific resources were also not
// specified.
if len(resources) == 0 {
log.Fatalf("No matching resources found for labels: %v", labels)
return
}
}
selectedResources := parseSelectedResources(resources)
syncReq := application.ApplicationSyncRequest{
Name: &appName,
DryRun: dryRun,
@@ -1117,18 +1177,21 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, selectedResources)
errors.CheckError(err)
pruningRequired := 0
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
if resDetails.Status == argoappv1.ResultCodePruneSkipped {
pruningRequired++
// Only get resources to be pruned if sync was application-wide
if len(selectedResources) == 0 {
pruningRequired := 0
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
if resDetails.Status == argoappv1.ResultCodePruneSkipped {
pruningRequired++
}
}
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
}
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
if !app.Status.OperationState.Phase.Successful() && !dryRun {
os.Exit(1)
if !app.Status.OperationState.Phase.Successful() && !dryRun {
os.Exit(1)
}
}
},
}
@@ -1136,6 +1199,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
command.Flags().StringArrayVar(&labels, "label", []string{}, fmt.Sprintf("Sync only specific resources with a label. This option may be specified repeatedly."))
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
command.Flags().BoolVar(&force, "force", false, "Use a force apply")

View File

@@ -0,0 +1,25 @@
package commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseLabels(t *testing.T) {
validLabels := []string{"key=value", "foo=bar", "intuit=inc"}
result, err := parseLabels(validLabels)
assert.NoError(t, err)
assert.Len(t, result, 3)
invalidLabels := []string{"key=value", "too=many=equals"}
_, err = parseLabels(invalidLabels)
assert.Error(t, err)
emptyLabels := []string{}
result, err = parseLabels(emptyLabels)
assert.NoError(t, err)
assert.Len(t, result, 0)
}

View File

@@ -110,18 +110,19 @@ func NewApplicationController(
}
appInformer, appLister := ctrl.newApplicationInformerAndLister()
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, func(appName string, fullRefresh bool) {
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister)
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, ctrl.metricsServer, func(appName string, fullRefresh bool) {
ctrl.requestAppRefresh(appName, fullRefresh)
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
})
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer)
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer, ctrl.metricsServer)
ctrl.appInformer = appInformer
ctrl.appLister = appLister
ctrl.projInformer = projInformer
ctrl.appStateManager = appStateManager
ctrl.stateCache = stateCache
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, ctrl.appLister)
return &ctrl, nil
}
@@ -349,9 +350,16 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
objs = append(objs, objsMap[k])
}
}
cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
if err != nil {
return err
}
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
err = util.RunAllAsync(len(objs), func(i int) error {
obj := objs[i]
return ctrl.stateCache.Delete(app.Spec.Destination.Server, obj)
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
})
if err != nil {
return err
@@ -688,7 +696,7 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
})
}
} else {
specConditions, _, err := argo.GetSpecErrors(context.Background(), &app.Spec, proj, ctrl.repoClientset, ctrl.db)
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
if err != nil {
conditions = append(conditions, appv1.ApplicationCondition{
Type: appv1.ApplicationConditionUnknownError,
@@ -927,6 +935,7 @@ func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
ctrl.settingsMgr.Subscribe(updateCh)
prevAppLabelKey := ctrl.settings.GetAppInstanceLabelKey()
prevResourceExclusions := ctrl.settings.ResourceExclusions
prevResourceInclusions := ctrl.settings.ResourceInclusions
done := false
for !done {
select {
@@ -939,10 +948,15 @@ func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
prevAppLabelKey = newAppLabelKey
}
if !reflect.DeepEqual(prevResourceExclusions, newSettings.ResourceExclusions) {
log.Infof("resource exclusions modified")
log.WithFields(log.Fields{"prevResourceExclusions": prevResourceExclusions, "newResourceExclusions": newSettings.ResourceExclusions}).Info("resource exclusions modified")
ctrl.stateCache.Invalidate()
prevResourceExclusions = newSettings.ResourceExclusions
}
if !reflect.DeepEqual(prevResourceInclusions, newSettings.ResourceInclusions) {
log.WithFields(log.Fields{"prevResourceInclusions": prevResourceInclusions, "newResourceInclusions": newSettings.ResourceInclusions}).Info("resource inclusions modified")
ctrl.stateCache.Invalidate()
prevResourceInclusions = newSettings.ResourceInclusions
}
case <-ctx.Done():
done = true
}

View File

@@ -10,6 +10,7 @@ import (
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
"github.com/argoproj/argo-cd/controller/metrics"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/db"
@@ -25,8 +26,6 @@ type LiveStateCache interface {
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
// Starts watching resources of each controlled cluster.
Run(ctx context.Context)
// Deletes specified resource from cluster.
Delete(server string, obj *unstructured.Unstructured) error
// Invalidate invalidates the entire cluster state cache
Invalidate()
}
@@ -42,26 +41,35 @@ func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isName
return key
}
func NewLiveStateCache(db db.ArgoDB, appInformer cache.SharedIndexInformer, settings *settings.ArgoCDSettings, kubectl kube.Kubectl, onAppUpdated func(appName string, fullRefresh bool)) LiveStateCache {
func NewLiveStateCache(
db db.ArgoDB,
appInformer cache.SharedIndexInformer,
settings *settings.ArgoCDSettings,
kubectl kube.Kubectl,
metricsServer *metrics.MetricsServer,
onAppUpdated func(appName string, fullRefresh bool)) LiveStateCache {
return &liveStateCache{
appInformer: appInformer,
db: db,
clusters: make(map[string]*clusterInfo),
lock: &sync.Mutex{},
onAppUpdated: onAppUpdated,
kubectl: kubectl,
settings: settings,
appInformer: appInformer,
db: db,
clusters: make(map[string]*clusterInfo),
lock: &sync.Mutex{},
onAppUpdated: onAppUpdated,
kubectl: kubectl,
settings: settings,
metricsServer: metricsServer,
}
}
type liveStateCache struct {
db db.ArgoDB
clusters map[string]*clusterInfo
lock *sync.Mutex
appInformer cache.SharedIndexInformer
onAppUpdated func(appName string, fullRefresh bool)
kubectl kube.Kubectl
settings *settings.ArgoCDSettings
db db.ArgoDB
clusters map[string]*clusterInfo
lock *sync.Mutex
appInformer cache.SharedIndexInformer
onAppUpdated func(appName string, fullRefresh bool)
kubectl kube.Kubectl
settings *settings.ArgoCDSettings
metricsServer *metrics.MetricsServer
}
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
@@ -116,14 +124,6 @@ func (c *liveStateCache) Invalidate() {
log.Info("live state cache invalidated")
}
func (c *liveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
return err
}
return clusterInfo.delete(obj)
}
func (c *liveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
@@ -146,7 +146,7 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
if err != nil {
return nil, err
}
return clusterInfo.getManagedLiveObjs(a, targetObjs)
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
}
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {

View File

@@ -7,6 +7,8 @@ import (
"sync"
"time"
"github.com/argoproj/argo-cd/controller/metrics"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -351,7 +353,7 @@ func (c *clusterInfo) isNamespaced(obj *unstructured.Unstructured) bool {
return true
}
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured, metricsServer *metrics.MetricsServer) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
c.lock.Lock()
defer c.lock.Unlock()
@@ -362,6 +364,7 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
managedObjs[key] = o.resource
}
}
config := metrics.AddMetricsTransportWrapper(metricsServer, a, c.cluster.RESTConfig())
// iterate target objects and identify ones that already exist in the cluster,\
// but are simply missing our label
lock := &sync.Mutex{}
@@ -378,7 +381,7 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
managedObj = existingObj.resource
} else {
var err error
managedObj, err = c.kubectl.GetResource(c.cluster.RESTConfig(), targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
if err != nil {
if errors.IsNotFound(err) {
return nil
@@ -390,9 +393,19 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
}
if managedObj != nil {
managedObj, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
if err != nil {
return err
// fallback to loading resource from kubernetes if conversion fails
log.Warnf("Failed to convert resource: %v", err)
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), managedObj.GetName(), managedObj.GetNamespace())
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
} else {
managedObj = converted
}
lock.Lock()
managedObjs[key] = managedObj
@@ -407,10 +420,6 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
return managedObjs, nil
}
func (c *clusterInfo) delete(obj *unstructured.Unstructured) error {
return c.kubectl.DeleteResource(c.cluster.RESTConfig(), obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
}
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
c.lock.Lock()
defer c.lock.Unlock()

View File

@@ -107,6 +107,10 @@ var (
serviceName: helm-guestbook
servicePort: 443
path: /
- backend:
serviceName: helm-guestbook
servicePort: https
path: /
status:
loadBalancer:
ingress:
@@ -225,7 +229,7 @@ metadata:
Namespace: "default",
},
},
}, []*unstructured.Unstructured{targetDeploy})
}, []*unstructured.Unstructured{targetDeploy}, nil)
assert.Nil(t, err)
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,

View File

@@ -63,14 +63,14 @@ func populateServiceInfo(un *unstructured.Unstructured, node *node) {
}
func populateIngressInfo(un *unstructured.Unstructured, node *node) {
targets := make([]v1alpha1.ResourceRef, 0)
targetsMap := make(map[v1alpha1.ResourceRef]bool)
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
targets = append(targets, v1alpha1.ResourceRef{
targetsMap[v1alpha1.ResourceRef{
Group: "",
Kind: kube.ServiceKind,
Namespace: un.GetNamespace(),
Name: fmt.Sprintf("%s", backend["serviceName"]),
})
}] = true
}
urlsSet := make(map[string]bool)
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
@@ -91,19 +91,19 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
}
if serviceName, ok, err := unstructured.NestedString(path, "backend", "serviceName"); ok && err == nil {
targets = append(targets, v1alpha1.ResourceRef{
targetsMap[v1alpha1.ResourceRef{
Group: "",
Kind: kube.ServiceKind,
Namespace: un.GetNamespace(),
Name: serviceName,
})
}] = true
}
if port, ok, err := unstructured.NestedFieldNoCopy(path, "backend", "servicePort"); ok && err == nil && host != "" {
switch fmt.Sprintf("%v", port) {
case "80":
case "80", "http":
urlsSet[fmt.Sprintf("http://%s", host)] = true
case "443":
case "443", "https":
urlsSet[fmt.Sprintf("https://%s", host)] = true
default:
urlsSet[fmt.Sprintf("http://%s:%s", host, port)] = true
@@ -112,6 +112,10 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
}
}
}
targets := make([]v1alpha1.ResourceRef, 0)
for target := range targetsMap {
targets = append(targets, target)
}
urls := make([]string, 0)
for url := range urlsSet {
urls = append(urls, url)

View File

@@ -2,6 +2,7 @@ package metrics
import (
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
@@ -17,6 +18,7 @@ import (
type MetricsServer struct {
*http.Server
syncCounter *prometheus.CounterVec
k8sRequestCounter *prometheus.CounterVec
reconcileHistogram *prometheus.HistogramVec
}
@@ -72,6 +74,14 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
append(descAppDefaultLabels, "phase"),
)
appRegistry.MustRegister(syncCounter)
k8sRequestCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_k8s_request_total",
Help: "Number of kubernetes requests executed during application reconciliation.",
},
append(descAppDefaultLabels, "response_code"),
)
appRegistry.MustRegister(k8sRequestCounter)
reconcileHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
@@ -91,6 +101,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
Handler: mux,
},
syncCounter: syncCounter,
k8sRequestCounter: k8sRequestCounter,
reconcileHistogram: reconcileHistogram,
}
}
@@ -103,6 +114,11 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
}
// IncKubernetesRequest increments the kubernetes requests counter for an application
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
}
// IncReconcile increments the reconcile counter for an application
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())

View File

@@ -0,0 +1,37 @@
package metrics
import (
"net/http"
"k8s.io/client-go/rest"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
type metricsRoundTripper struct {
roundTripper http.RoundTripper
app *v1alpha1.Application
metricsServer *MetricsServer
}
func (mrt *metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := mrt.roundTripper.RoundTrip(r)
statusCode := 0
if resp != nil {
statusCode = resp.StatusCode
}
mrt.metricsServer.IncKubernetesRequest(mrt.app, statusCode)
return resp, err
}
// AddMetricsTransportWrapper adds a transport wrapper which increments 'argocd_app_k8s_request_total' counter on each kubernetes request
func AddMetricsTransportWrapper(server *MetricsServer, app *v1alpha1.Application, config *rest.Config) *rest.Config {
wrap := config.WrapTransport
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
if wrap != nil {
rt = wrap(rt)
}
return &metricsRoundTripper{roundTripper: rt, metricsServer: server, app: app}
}
return config
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/argoproj/argo-cd/common"
statecache "github.com/argoproj/argo-cd/controller/cache"
"github.com/argoproj/argo-cd/controller/metrics"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
@@ -73,6 +74,7 @@ type comparisonResult struct {
// appStateManager allows to compare applications to git
type appStateManager struct {
metricsServer *metrics.MetricsServer
db db.ArgoDB
settings *settings.ArgoCDSettings
appclientset appclientset.Interface
@@ -379,6 +381,7 @@ func NewAppStateManager(
settings *settings.ArgoCDSettings,
liveStateCache statecache.LiveStateCache,
projInformer cache.SharedIndexInformer,
metricsServer *metrics.MetricsServer,
) AppStateManager {
return &appStateManager{
liveStateCache: liveStateCache,
@@ -389,5 +392,6 @@ func NewAppStateManager(
namespace: namespace,
settings: settings,
projInformer: projInformer,
metricsServer: metricsServer,
}
}

View File

@@ -14,6 +14,7 @@ import (
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"github.com/argoproj/argo-cd/controller/metrics"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo"
@@ -116,7 +117,7 @@ func (m *appStateManager) SyncAppState(app *appv1.Application, state *appv1.Oper
return
}
restConfig := clst.RESTConfig()
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
dynamicIf, err := dynamic.NewForConfig(restConfig)
if err != nil {
state.Phase = appv1.OperationError

View File

@@ -150,6 +150,88 @@ data:
key: sshPrivateKey
```
!!! tip
The Kubernetes documentation has [instructions for creating a secret containing a private key](https://kubernetes.io/docs/concepts/configuration/secret/#use-case-pod-with-ssh-keys).
### Repository Credentials (v1.1+)
If you want to use the same credentials for multiple repositories, you can use `repository.credentials`:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
data:
repositories: |
- url: https://github.com/argoproj/private-repo
- url: https://github.com/argoproj/other-private-repo
repository.credentials: |
- url: https://github.com/argoproj
passwordSecret:
name: my-secret
key: password
usernameSecret:
name: my-secret
key: username
```
Argo CD will only use the credentials if you omit `usernameSecret`, `passwordSecret`, and `sshPrivateKeySecret` fields (`insecureIgnoreHostKey` is ignored).
A credential may be match if it's URL is the prefix of the repository's URL. The means that credentials may match, e.g in the above example both [https://github.com/argoproj](https://github.com/argoproj) and [https://github.com](https://github.com) would match. Argo CD selects the first one that matches.
!!! tip
Order your credentials with the most specific at the top and the least specific at the bottom.
A complete example.
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
data:
repositories: |
# this has it's own credentials
- url: https://github.com/argoproj/private-repo
passwordSecret:
name: private-repo-secret
key: password
usernameSecret:
name: private-repo-secret
key: username
sshPrivateKeySecret:
name: private-repo-secret
key: sshPrivateKey
- url: https://github.com/argoproj/other-private-repo
- url: https://github.com/otherproj/another-private-repo
repository.credentials: |
# this will be used for the second repo
- url: https://github.com/argoproj
passwordSecret:
name: other-private-repo-secret
key: password
usernameSecret:
name: other-private-repo-secret
key: username
sshPrivateKeySecret:
name: other-private-repo-secret
key: sshPrivateKey
# this will be used for the third repo
- url: https://github.com
passwordSecret:
name: another-private-repo-secret
key: password
usernameSecret:
name: another-private-repo-secret
key: username
sshPrivateKeySecret:
name: another-private-repo-secret
key: sshPrivateKey
```
## Clusters
Cluster credentials are stored in secrets same as repository credentials but does not require entry in `argocd-cm` config map. Each secret must have label

View File

@@ -12,7 +12,7 @@ bases:
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: latest
newTag: v1.0.0-rc2
- name: argoproj/argocd-ui
newName: argoproj/argocd-ui
newTag: latest
newTag: v1.0.0-rc2

View File

@@ -17,7 +17,7 @@ patchesStrategicMerge:
images:
- name: argoproj/argocd
newName: argoproj/argocd
newTag: latest
newTag: v1.0.0-rc2
- name: argoproj/argocd-ui
newName: argoproj/argocd-ui
newTag: latest
newTag: v1.0.0-rc2

View File

@@ -185,10 +185,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -419,10 +415,6 @@ spec:
description: NamePrefix is a prefix appended to resources for
kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository containing
@@ -488,6 +480,12 @@ spec:
- message
type: object
type: array
externalURLs:
description: ExternalURLs holds all external URLs of application child
resources.
items:
type: string
type: array
health:
properties:
message:
@@ -633,10 +631,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -670,9 +664,6 @@ spec:
- source
type: object
type: array
ingress:
items: {}
type: array
observedAt: {}
operationState:
description: OperationState contains information about state of currently
@@ -853,10 +844,6 @@ spec:
description: NamePrefix is a prefix appended to
resources for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1092,10 +1079,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1312,10 +1295,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -2138,7 +2117,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:
@@ -2185,7 +2164,7 @@ spec:
- cp
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2240,7 +2219,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-repo-server
ports:
@@ -2297,7 +2276,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-server
ports:
@@ -2318,7 +2297,7 @@ spec:
- -r
- /app
- /shared
image: argoproj/argocd-ui:latest
image: argoproj/argocd-ui:v1.0.0-rc2
imagePullPolicy: Always
name: ui
volumeMounts:

View File

@@ -185,10 +185,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -419,10 +415,6 @@ spec:
description: NamePrefix is a prefix appended to resources for
kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository containing
@@ -488,6 +480,12 @@ spec:
- message
type: object
type: array
externalURLs:
description: ExternalURLs holds all external URLs of application child
resources.
items:
type: string
type: array
health:
properties:
message:
@@ -633,10 +631,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -670,9 +664,6 @@ spec:
- source
type: object
type: array
ingress:
items: {}
type: array
observedAt: {}
operationState:
description: OperationState contains information about state of currently
@@ -853,10 +844,6 @@ spec:
description: NamePrefix is a prefix appended to
resources for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1092,10 +1079,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1312,10 +1295,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -2053,7 +2032,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:
@@ -2100,7 +2079,7 @@ spec:
- cp
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2155,7 +2134,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-repo-server
ports:
@@ -2212,7 +2191,7 @@ spec:
- argocd-redis-ha-announce-2:26379
- --sentinelmaster
- argocd
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-server
ports:
@@ -2233,7 +2212,7 @@ spec:
- -r
- /app
- /shared
image: argoproj/argocd-ui:latest
image: argoproj/argocd-ui:v1.0.0-rc2
imagePullPolicy: Always
name: ui
volumeMounts:

View File

@@ -185,10 +185,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -419,10 +415,6 @@ spec:
description: NamePrefix is a prefix appended to resources for
kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository containing
@@ -488,6 +480,12 @@ spec:
- message
type: object
type: array
externalURLs:
description: ExternalURLs holds all external URLs of application child
resources.
items:
type: string
type: array
health:
properties:
message:
@@ -633,10 +631,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -670,9 +664,6 @@ spec:
- source
type: object
type: array
ingress:
items: {}
type: array
observedAt: {}
operationState:
description: OperationState contains information about state of currently
@@ -853,10 +844,6 @@ spec:
description: NamePrefix is a prefix appended to
resources for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1092,10 +1079,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1312,10 +1295,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1902,7 +1881,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:
@@ -1949,7 +1928,7 @@ spec:
- cp
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -2012,7 +1991,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-repo-server
ports:
@@ -2046,7 +2025,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-server
ports:
@@ -2067,7 +2046,7 @@ spec:
- -r
- /app
- /shared
image: argoproj/argocd-ui:latest
image: argoproj/argocd-ui:v1.0.0-rc2
imagePullPolicy: Always
name: ui
volumeMounts:

View File

@@ -185,10 +185,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -419,10 +415,6 @@ spec:
description: NamePrefix is a prefix appended to resources for
kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository containing
@@ -488,6 +480,12 @@ spec:
- message
type: object
type: array
externalURLs:
description: ExternalURLs holds all external URLs of application child
resources.
items:
type: string
type: array
health:
properties:
message:
@@ -633,10 +631,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -670,9 +664,6 @@ spec:
- source
type: object
type: array
ingress:
items: {}
type: array
observedAt: {}
operationState:
description: OperationState contains information about state of currently
@@ -853,10 +844,6 @@ spec:
description: NamePrefix is a prefix appended to
resources for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1092,10 +1079,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1312,10 +1295,6 @@ spec:
description: NamePrefix is a prefix appended to resources
for kustomize apps
type: string
required:
- namePrefix
- imageTags
- images
type: object
path:
description: Path is a directory path within the repository
@@ -1817,7 +1796,7 @@ spec:
- "20"
- --operation-processors
- "10"
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-application-controller
ports:
@@ -1864,7 +1843,7 @@ spec:
- cp
- /usr/local/bin/argocd-util
- /shared
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: copyutil
volumeMounts:
@@ -1927,7 +1906,7 @@ spec:
- argocd-repo-server
- --redis
- argocd-redis:6379
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-repo-server
ports:
@@ -1961,7 +1940,7 @@ spec:
- argocd-server
- --staticassets
- /shared/app
image: argoproj/argocd:latest
image: argoproj/argocd:v1.0.0-rc2
imagePullPolicy: Always
name: argocd-server
ports:
@@ -1982,7 +1961,7 @@ spec:
- -r
- /app
- /shared
image: argoproj/argocd-ui:latest
image: argoproj/argocd-ui:v1.0.0-rc2
imagePullPolicy: Always
name: ui
volumeMounts:

View File

@@ -104,6 +104,7 @@ func schema_pkg_apis_application_v1alpha1_AWSAuthConfig(ref common.ReferenceCall
},
},
},
Dependencies: []string{},
}
}
@@ -357,6 +358,7 @@ func schema_pkg_apis_application_v1alpha1_ApplicationCondition(ref common.Refere
Required: []string{"type", "message"},
},
},
Dependencies: []string{},
}
}
@@ -384,6 +386,7 @@ func schema_pkg_apis_application_v1alpha1_ApplicationDestination(ref common.Refe
},
},
},
Dependencies: []string{},
}
}
@@ -709,6 +712,7 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSourcePlugin(ref common.Ref
},
},
},
Dependencies: []string{},
}
}
@@ -1076,6 +1080,7 @@ func schema_pkg_apis_application_v1alpha1_Command(ref common.ReferenceCallback)
},
},
},
Dependencies: []string{},
}
}
@@ -1134,6 +1139,7 @@ func schema_pkg_apis_application_v1alpha1_ComponentParameter(ref common.Referenc
Required: []string{"name", "value"},
},
},
Dependencies: []string{},
}
}
@@ -1223,6 +1229,7 @@ func schema_pkg_apis_application_v1alpha1_HealthStatus(ref common.ReferenceCallb
},
},
},
Dependencies: []string{},
}
}
@@ -1250,6 +1257,7 @@ func schema_pkg_apis_application_v1alpha1_HelmParameter(ref common.ReferenceCall
},
},
},
Dependencies: []string{},
}
}
@@ -1305,6 +1313,7 @@ func schema_pkg_apis_application_v1alpha1_HelmRepository(ref common.ReferenceCal
Required: []string{"url", "name"},
},
},
Dependencies: []string{},
}
}
@@ -1332,6 +1341,7 @@ func schema_pkg_apis_application_v1alpha1_InfoItem(ref common.ReferenceCallback)
},
},
},
Dependencies: []string{},
}
}
@@ -1358,6 +1368,7 @@ func schema_pkg_apis_application_v1alpha1_JWTToken(ref common.ReferenceCallback)
Required: []string{"iat"},
},
},
Dependencies: []string{},
}
}
@@ -1390,6 +1401,7 @@ func schema_pkg_apis_application_v1alpha1_JsonnetVar(ref common.ReferenceCallbac
Required: []string{"name", "value"},
},
},
Dependencies: []string{},
}
}
@@ -1422,6 +1434,7 @@ func schema_pkg_apis_application_v1alpha1_KsonnetParameter(ref common.ReferenceC
Required: []string{"name", "value"},
},
},
Dependencies: []string{},
}
}
@@ -1449,6 +1462,7 @@ func schema_pkg_apis_application_v1alpha1_KustomizeImageTag(ref common.Reference
},
},
},
Dependencies: []string{},
}
}
@@ -1735,6 +1749,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceActionDefinition(ref common.Re
Required: []string{"name", "action.lua"},
},
},
Dependencies: []string{},
}
}
@@ -1771,6 +1786,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceActionParam(ref common.Referen
},
},
},
Dependencies: []string{},
}
}
@@ -1859,6 +1875,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceDiff(ref common.ReferenceCallb
},
},
},
Dependencies: []string{},
}
}
@@ -1910,6 +1927,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceIgnoreDifferences(ref common.R
Required: []string{"group", "kind", "jsonPointers"},
},
},
Dependencies: []string{},
}
}
@@ -1924,7 +1942,6 @@ func schema_pkg_apis_application_v1alpha1_ResourceNetworkingInfo(ref common.Refe
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
@@ -1950,7 +1967,6 @@ func schema_pkg_apis_application_v1alpha1_ResourceNetworkingInfo(ref common.Refe
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
@@ -2120,6 +2136,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceOverride(ref common.ReferenceC
},
},
},
Dependencies: []string{},
}
}
@@ -2163,6 +2180,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceRef(ref common.ReferenceCallba
},
},
},
Dependencies: []string{},
}
}
@@ -2231,6 +2249,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceResult(ref common.ReferenceCal
Required: []string{"group", "version", "kind", "namespace", "name"},
},
},
Dependencies: []string{},
}
}
@@ -2424,6 +2443,7 @@ func schema_pkg_apis_application_v1alpha1_SyncOperationResource(ref common.Refer
Required: []string{"kind", "name"},
},
},
Dependencies: []string{},
}
}
@@ -2507,6 +2527,7 @@ func schema_pkg_apis_application_v1alpha1_SyncPolicyAutomated(ref common.Referen
},
},
},
Dependencies: []string{},
}
}
@@ -2587,6 +2608,7 @@ func schema_pkg_apis_application_v1alpha1_SyncStrategyApply(ref common.Reference
},
},
},
Dependencies: []string{},
}
}
@@ -2657,6 +2679,7 @@ func schema_pkg_apis_application_v1alpha1_TLSClientConfig(ref common.ReferenceCa
Required: []string{"insecure"},
},
},
Dependencies: []string{},
}
}
@@ -2677,5 +2700,6 @@ func schema_pkg_apis_application_v1alpha1_objectMeta(ref common.ReferenceCallbac
Required: []string{"Name"},
},
},
Dependencies: []string{},
}
}

View File

@@ -756,6 +756,17 @@ type Repository struct {
InsecureIgnoreHostKey bool `json:"insecureIgnoreHostKey,omitempty" protobuf:"bytes,6,opt,name=insecureIgnoreHostKey"`
}
func (m *Repository) HasCredentials() bool {
return m.Username != "" || m.Password != "" || m.SSHPrivateKey != "" || m.InsecureIgnoreHostKey
}
func (m *Repository) CopyCredentialsFrom(source Repository) {
m.Username = source.Username
m.Password = source.Password
m.SSHPrivateKey = source.SSHPrivateKey
m.InsecureIgnoreHostKey = source.InsecureIgnoreHostKey
}
// RepositoryList is a collection of Repositories.
type RepositoryList struct {
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

View File

@@ -199,3 +199,87 @@ func TestAppProjectSpec_DestinationClusters(t *testing.T) {
})
}
}
func TestRepository_HasCredentials(t *testing.T) {
tests := []struct {
name string
repo Repository
want bool
}{
{
name: "TestHasRepo",
repo: Repository{Repo: "foo"},
want: false,
},
{
name: "TestHasUsername",
repo: Repository{Username: "foo"},
want: true,
},
{
name: "TestHasPassword",
repo: Repository{Password: "foo"},
want: true,
},
{
name: "TestHasSSHPrivateKey",
repo: Repository{SSHPrivateKey: "foo"},
want: true,
},
{
name: "TestHasInsecureHostKey",
repo: Repository{InsecureIgnoreHostKey: true},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.repo.HasCredentials(); got != tt.want {
t.Errorf("Repository.HasCredentials() = %v, want %v", got, tt.want)
}
})
}
}
func TestRepository_CopyCredentialsFrom(t *testing.T) {
tests := []struct {
name string
source Repository
want Repository
}{
{
name: "TestHasRepo",
source: Repository{Repo: "foo"},
want: Repository{},
},
{
name: "TestHasUsername",
source: Repository{Username: "foo"},
want: Repository{Username: "foo"},
},
{
name: "TestHasPassword",
source: Repository{Password: "foo"},
want: Repository{Password: "foo"},
},
{
name: "TestHasSSHPrivateKey",
source: Repository{SSHPrivateKey: "foo"},
want: Repository{SSHPrivateKey: "foo"},
},
{
name: "TestHasInsecureHostKey",
source: Repository{InsecureIgnoreHostKey: true},
want: Repository{InsecureIgnoreHostKey: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := Repository{}
repo.CopyCredentialsFrom(tt.source)
assert.Equal(t, tt.want, repo)
})
}
}

View File

@@ -5,13 +5,13 @@ import (
"time"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"github.com/argoproj/argo-cd/reposerver/repository"
"github.com/argoproj/argo-cd/util"
argogrpc "github.com/argoproj/argo-cd/util/grpc"
)
// Clientset represets repository server api clients
@@ -20,7 +20,8 @@ type Clientset interface {
}
type clientSet struct {
address string
address string
timeoutSeconds int
}
func (c *clientSet) NewRepoServerClient() (util.Closer, repository.RepoServerServiceClient, error) {
@@ -28,10 +29,14 @@ func (c *clientSet) NewRepoServerClient() (util.Closer, repository.RepoServerSer
grpc_retry.WithMax(3),
grpc_retry.WithBackoff(grpc_retry.BackoffLinear(1000 * time.Millisecond)),
}
conn, err := grpc.Dial(c.address,
opts := []grpc.DialOption{
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})),
grpc.WithStreamInterceptor(grpc_retry.StreamClientInterceptor(retryOpts...)),
grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...)))
grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...))}
if c.timeoutSeconds > 0 {
opts = append(opts, grpc.WithUnaryInterceptor(argogrpc.WithTimeout(time.Duration(c.timeoutSeconds)*time.Second)))
}
conn, err := grpc.Dial(c.address, opts...)
if err != nil {
log.Errorf("Unable to connect to repository service with address %s", c.address)
return nil, nil, err
@@ -40,6 +45,6 @@ func (c *clientSet) NewRepoServerClient() (util.Closer, repository.RepoServerSer
}
// NewRepoServerClientset creates new instance of repo server Clientset
func NewRepoServerClientset(address string) Clientset {
return &clientSet{address: address}
func NewRepoServerClientset(address string, timeoutSeconds int) Clientset {
return &clientSet{address: address, timeoutSeconds: timeoutSeconds}
}

View File

@@ -2,6 +2,9 @@ discoveryTests:
- inputPath: testdata/paused_rollout.yaml
result:
- name: resume
- inputPath: testdata/v0.2_paused_rollout.yaml
result:
- name: resume
- inputPath: testdata/not_paused_rollout.yaml
result: []
- inputPath: testdata/nil_paused_rollout.yaml
@@ -10,4 +13,6 @@ actionTests:
- action: resume
inputPath: testdata/paused_rollout.yaml
expectedOutputPath: testdata/not_paused_rollout.yaml
- action: resume
inputPath: testdata/v0.2_paused_rollout.yaml
expectedOutputPath: testdata/v0.2_not_paused_rollout.yaml

View File

@@ -1,6 +1,13 @@
actions = {}
if obj.spec.paused ~= nil and obj.spec.paused then
local paused = false
if obj.status.verifyingPreview ~= nil then
paused = obj.status.verifyingPreview
elseif obj.spec.paused ~= nil then
paused = obj.spec.paused
end
if paused then
actions["resume"] = {}
end

View File

@@ -1,2 +1,9 @@
obj.spec.paused = false
if obj.status.verifyingPreview ~= nil and obj.status.verifyingPreview then
obj.status.verifyingPreview = false
end
if obj.spec.paused ~= nil and obj.spec.paused then
obj.spec.paused = false
end
return obj

View File

@@ -0,0 +1,55 @@
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
annotations:
rollout.argoproj.io/revision: "7"
clusterName: ""
creationTimestamp: 2019-01-22T16:52:54Z
generation: 1
labels:
app.kubernetes.io/instance: guestbook-default
name: ks-guestbook-ui
namespace: default
resourceVersion: "164113"
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
uid: 29802403-1e66-11e9-a6a4-025000000001
spec:
minReadySeconds: 30
replicas: 3
selector:
matchLabels:
app: ks-guestbook-ui
strategy:
blueGreen:
activeService: ks-guestbook-ui-active
previewService: ks-guestbook-ui-preview
type: BlueGreenUpdate
template:
metadata:
labels:
app: ks-guestbook-ui
spec:
containers:
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
name: ks-guestbook-ui
ports:
- containerPort: 83
resources: {}
status:
blueGreen:
activeSelector: 85f9884f5d
previewSelector: 697fb9575c
availableReplicas: 6
conditions:
- lastTransitionTime: 2019-01-25T07:44:26Z
lastUpdateTime: 2019-01-25T07:44:26Z
message: Rollout is serving traffic from the active service.
reason: Available
status: "True"
type: Available
currentPodHash: 697fb9575c
observedGeneration: 767f98959f
readyReplicas: 6
replicas: 6
updatedReplicas: 3
verifyingPreview: false

View File

@@ -0,0 +1,55 @@
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
annotations:
rollout.argoproj.io/revision: "7"
clusterName: ""
creationTimestamp: 2019-01-22T16:52:54Z
generation: 1
labels:
app.kubernetes.io/instance: guestbook-default
name: ks-guestbook-ui
namespace: default
resourceVersion: "164113"
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
uid: 29802403-1e66-11e9-a6a4-025000000001
spec:
minReadySeconds: 30
replicas: 3
selector:
matchLabels:
app: ks-guestbook-ui
strategy:
blueGreen:
activeService: ks-guestbook-ui-active
previewService: ks-guestbook-ui-preview
type: BlueGreenUpdate
template:
metadata:
labels:
app: ks-guestbook-ui
spec:
containers:
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
name: ks-guestbook-ui
ports:
- containerPort: 83
resources: {}
status:
blueGreen:
activeSelector: 85f9884f5d
previewSelector: 697fb9575c
availableReplicas: 6
conditions:
- lastTransitionTime: 2019-01-25T07:44:26Z
lastUpdateTime: 2019-01-25T07:44:26Z
message: Rollout is serving traffic from the active service.
reason: Available
status: "True"
type: Available
currentPodHash: 697fb9575c
observedGeneration: 767f98959f
readyReplicas: 6
replicas: 6
updatedReplicas: 3
verifyingPreview: true

View File

@@ -16,7 +16,14 @@ if obj.status ~= nil then
return hs
end
if obj.spec.paused then
local paused = false
if obj.status.verifyingPreview ~= nil then
paused = obj.status.verifyingPreview
elseif obj.spec.paused ~= nil then
paused = obj.spec.paused
end
if paused then
hs.status = "Suspended"
if obj.status.blueGreen.previewSelector ~= nil and obj.status.blueGreen.previewSelector == obj.status.currentPodHash then
hs.message = "The preview Service is serving traffic to the current pod spec"

View File

@@ -7,6 +7,10 @@ tests:
status: Suspended
message: The preview Service is serving traffic to the current pod spec
inputPath: testdata/suspended_servingPreviewService.yaml
- healthStatus:
status: Suspended
message: The preview Service is serving traffic to the current pod spec
inputPath: testdata/v0.2_suspended_servingPreviewService.yaml
- healthStatus:
status: Progressing
message: "Waiting for roll out to finish: More replicas need to be updated"

View File

@@ -0,0 +1,56 @@
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
annotations:
rollout.argoproj.io/revision: "7"
clusterName: ""
creationTimestamp: 2019-01-22T16:52:54Z
generation: 1
labels:
app.kubernetes.io/instance: guestbook-default
name: ks-guestbook-ui
namespace: default
resourceVersion: "164113"
selfLink: /apis/argoproj.io/v1alpha1/namespaces/default/rollouts/ks-guestbook-ui
uid: 29802403-1e66-11e9-a6a4-025000000001
spec:
minReadySeconds: 30
replicas: 3
selector:
matchLabels:
app: ks-guestbook-ui
strategy:
blueGreen:
activeService: ks-guestbook-ui-active
previewService: ks-guestbook-ui-preview
type: BlueGreenUpdate
template:
metadata:
creationTimestamp: null
labels:
app: ks-guestbook-ui
spec:
containers:
- image: gcr.io/heptio-images/ks-guestbook-demo:0.1
name: ks-guestbook-ui
ports:
- containerPort: 83
resources: {}
status:
blueGreen:
activeSelector: 85f9884f5d
previewSelector: 697fb9575c
availableReplicas: 6
conditions:
- lastTransitionTime: 2019-01-25T07:44:26Z
lastUpdateTime: 2019-01-25T07:44:26Z
message: Rollout is serving traffic from the active service.
reason: Available
status: "True"
type: Available
currentPodHash: 697fb9575c
observedGeneration: 767f98959f
readyReplicas: 6
replicas: 6
updatedReplicas: 3
verifyingPreview: true

View File

@@ -501,7 +501,14 @@ func (s *Server) validateAndNormalizeApp(ctx context.Context, app *appv1.Applica
}
}
conditions, appSourceType, err := argo.GetSpecErrors(ctx, &app.Spec, proj, s.repoClientset, s.db)
conditions, err := argo.ValidatePermissions(ctx, &app.Spec, proj, s.db)
if err != nil {
return err
}
if len(conditions) > 0 {
return status.Errorf(codes.InvalidArgument, "application spec is invalid: %s", argo.FormatAppConditions(conditions))
}
conditions, appSourceType, err := argo.ValidateRepo(ctx, &app.Spec, s.repoClientset, s.db)
if err != nil {
return err
}

View File

@@ -406,6 +406,7 @@ func TestEdgeCasesApplicationResources(t *testing.T) {
"DeprecatedExtensions": "deprecated-extensions",
"CRDs": "crd-creation",
"DuplicatedResources": "duplicated-resources",
"FailedConversion": "failed-conversion",
}
for name, appPath := range apps {
@@ -516,3 +517,75 @@ func TestResourceAction(t *testing.T) {
assert.Equal(t, "test", deployment.Labels["sample"])
}
func TestSyncResourceByLabel(t *testing.T) {
fixture.EnsureCleanState()
app := createAndSyncDefault(t)
res, _ := fixture.RunCli("app", "sync", app.Name, "--label",
fmt.Sprintf("app.kubernetes.io/instance=test-%s", strings.Split(app.Name, "-")[1]))
assert.Contains(t, res, "guestbook-ui Synced Healthy")
res, _ = fixture.RunCli("app", "sync", app.Name, "--label", "this-label=does-not-exist")
assert.Contains(t, res, "level=fatal")
}
func TestPermissions(t *testing.T) {
fixture.EnsureCleanState()
appName := "test-app"
_, err := fixture.RunCli("proj", "create", "test")
assert.NoError(t, err)
// make sure app cannot be created without permissions in project
output, err := fixture.RunCli("app", "create", appName, "--repo", fixture.RepoURL(),
"--path", guestbookPath, "--project", "test", "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace)
assert.Error(t, err)
sourceError := fmt.Sprintf("application repo %s is not permitted in project 'test'", fixture.RepoURL())
destinationError := fmt.Sprintf("application destination {%s %s} is not permitted in project 'test'", common.KubernetesInternalAPIServerAddr, fixture.DeploymentNamespace)
assert.Contains(t, output, sourceError)
assert.Contains(t, output, destinationError)
proj, err := fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Get("test", metav1.GetOptions{})
assert.NoError(t, err)
proj.Spec.Destinations = []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}
proj.Spec.SourceRepos = []string{"*"}
proj, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Update(proj)
assert.NoError(t, err)
// make sure controller report permissions issues in conditions
_, err = fixture.RunCli("app", "create", "test-app", "--repo", fixture.RepoURL(),
"--path", guestbookPath, "--project", "test", "--dest-server", common.KubernetesInternalAPIServerAddr, "--dest-namespace", fixture.DeploymentNamespace)
assert.NoError(t, err)
defer func() {
err = fixture.AppClientset.ArgoprojV1alpha1().Applications(fixture.ArgoCDNamespace).Delete(appName, &metav1.DeleteOptions{})
assert.NoError(t, err)
}()
proj.Spec.Destinations = []v1alpha1.ApplicationDestination{}
proj.Spec.SourceRepos = []string{}
_, err = fixture.AppClientset.ArgoprojV1alpha1().AppProjects(fixture.ArgoCDNamespace).Update(proj)
assert.NoError(t, err)
closer, client, err := fixture.ArgoCDClientset.NewApplicationClient()
assert.NoError(t, err)
defer util.Close(closer)
refresh := string(v1alpha1.RefreshTypeNormal)
app, err := client.Get(context.Background(), &application.ApplicationQuery{Name: &appName, Refresh: &refresh})
assert.NoError(t, err)
destinationErrorExist := false
sourceErrorExist := false
for i := range app.Status.Conditions {
if strings.Contains(app.Status.Conditions[i].Message, destinationError) {
destinationErrorExist = true
}
if strings.Contains(app.Status.Conditions[i].Message, sourceError) {
sourceErrorExist = true
}
}
assert.True(t, destinationErrorExist)
assert.True(t, sourceErrorExist)
}

View File

@@ -0,0 +1,16 @@
apiVersion: apiregistration.k8s.io/v1beta1
kind: APIService
metadata:
name: v1beta1.metrics.k8s.io
labels:
app: metrics-server
chart: metrics-server
spec:
service:
name: metrics-server
namespace: kube-system
group: metrics.k8s.io
version: v1beta1
insecureSkipTLSVerify: true
groupPriorityMinimum: 100
versionPriority: 100

View File

@@ -127,28 +127,13 @@ func WaitForRefresh(ctx context.Context, appIf v1alpha1.ApplicationInterface, na
return nil, fmt.Errorf("application refresh deadline exceeded")
}
// GetSpecErrors returns list of conditions which indicates that app spec is invalid. Following is checked:
// ValidateRepo validates the repository specified in application spec. Following is checked:
// * the git repository is accessible
// * the git path contains valid manifests
// * the referenced cluster has been added to Argo CD
// * the app source repo and destination namespace/cluster are permitted in app project
// * there are parameters of only one app source type
// * ksonnet: the specified environment exists
func GetSpecErrors(
ctx context.Context,
spec *argoappv1.ApplicationSpec,
proj *argoappv1.AppProject,
repoClientset reposerver.Clientset,
db db.ArgoDB,
) ([]argoappv1.ApplicationCondition, argoappv1.ApplicationSourceType, error) {
func ValidateRepo(ctx context.Context, spec *argoappv1.ApplicationSpec, repoClientset reposerver.Clientset, db db.ArgoDB) ([]argoappv1.ApplicationCondition, argoappv1.ApplicationSourceType, error) {
conditions := make([]argoappv1.ApplicationCondition, 0)
if spec.Source.RepoURL == "" || spec.Source.Path == "" {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: "spec.source.repoURL and spec.source.path are required",
})
return conditions, "", nil
}
// Test the repo
conn, repoClient, err := repoClientset.NewRepoServerClient()
@@ -218,18 +203,31 @@ func GetSpecErrors(
conditions = append(conditions, helmConditions...)
}
case argoappv1.ApplicationSourceTypeDirectory, argoappv1.ApplicationSourceTypeKustomize:
maniDirConditions := verifyGenerateManifests(ctx, repoRes, []*argoappv1.HelmRepository{}, spec, repoClient)
if len(maniDirConditions) > 0 {
conditions = append(conditions, maniDirConditions...)
mainDirConditions := verifyGenerateManifests(ctx, repoRes, []*argoappv1.HelmRepository{}, spec, repoClient)
if len(mainDirConditions) > 0 {
conditions = append(conditions, mainDirConditions...)
}
}
}
}
return conditions, appSourceType, nil
}
// 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.Source.RepoURL == "" || spec.Source.Path == "" {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: "spec.source.repoURL and spec.source.path are required",
})
return conditions, nil
}
if !proj.IsSourcePermitted(spec.Source) {
conditions = append(conditions, argoappv1.ApplicationCondition{
Type: argoappv1.ApplicationConditionInvalidSpecError,
Message: fmt.Sprintf("application source %v is not permitted in project '%s'", spec.Source, spec.Project),
Message: fmt.Sprintf("application repo %s is not permitted in project '%s'", spec.Source.RepoURL, spec.Project),
})
}
@@ -241,7 +239,7 @@ func GetSpecErrors(
})
}
// Ensure the k8s cluster the app is referencing, is configured in Argo CD
_, err = db.GetCluster(ctx, spec.Destination.Server)
_, err := db.GetCluster(ctx, spec.Destination.Server)
if err != nil {
if errStatus, ok := status.FromError(err); ok && errStatus.Code() == codes.NotFound {
conditions = append(conditions, argoappv1.ApplicationCondition{
@@ -249,11 +247,11 @@ func GetSpecErrors(
Message: fmt.Sprintf("cluster '%s' has not been configured", spec.Destination.Server),
})
} else {
return nil, "", err
return nil, err
}
}
}
return conditions, appSourceType, nil
return conditions, nil
}
// GetAppProject returns a project from an application
@@ -340,6 +338,7 @@ func verifyAppYAML(ctx context.Context, repoRes *argoappv1.Repository, spec *arg
return nil
}
// verifyHelmChart verifies a helm chart is functional
// verifyHelmChart verifies a helm chart is functional
func verifyHelmChart(ctx context.Context, repoRes *argoappv1.Repository, spec *argoappv1.ApplicationSpec, repoClient repository.RepoServerServiceClient) []argoappv1.ApplicationCondition {
var conditions []argoappv1.ApplicationCondition

View File

@@ -4,7 +4,7 @@ import (
"encoding/json"
"strings"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/diff"
@@ -14,10 +14,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
noMatchingPathError = "Unable to remove nonexistent key: not-matching-path"
)
type normalizerPatch struct {
groupKind schema.GroupKind
namespace string
@@ -103,7 +99,8 @@ func (n *normalizer) Normalize(un *unstructured.Unstructured) error {
for _, patch := range matched {
patchedData, err := patch.patch.Apply(docData)
if err != nil {
if err.Error() == noMatchingPathError {
// bug?
if strings.HasPrefix(err.Error(), "Unable to remove nonexistent key") {
continue
}
return err

View File

@@ -73,3 +73,17 @@ func TestNormalizeMatchedResourceOverrides(t *testing.T) {
assert.Nil(t, err)
assert.False(t, has)
}
func TestNormalizeMissingJsonPointer(t *testing.T) {
normalizer, err := NewDiffNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
"apps/Deployment": {
IgnoreDifferences: `jsonPointers: ["/garbage"]`,
},
})
assert.NoError(t, err)
deployment := kube.MustToUnstructured(test.DemoDeployment())
err = normalizer.Normalize(deployment)
assert.NoError(t, err)
}

View File

@@ -2,6 +2,7 @@ package db
import (
"context"
"reflect"
"strings"
"testing"
@@ -80,10 +81,14 @@ func TestCreateExistingRepository(t *testing.T) {
assert.Equal(t, codes.AlreadyExists, status.Convert(err).Code())
}
func TestDeleteRepositoryManagedSecrets(t *testing.T) {
func TestGetRepository(t *testing.T) {
config := map[string]string{
"repositories": `
- url: https://github.com/argoproj/argocd-example-apps
- url: https://known/repo
- url: https://secured/repo
`,
"repository.credentials": `
- url: https://secured
usernameSecret:
name: managed-secret
key: username
@@ -91,7 +96,47 @@ func TestDeleteRepositoryManagedSecrets(t *testing.T) {
name: managed-secret
key: password
`}
clientset := getClientset(config, &v1.Secret{
clientset := getClientset(config, newManagedSecret())
db := NewDB(testNamespace, settings.NewSettingsManager(context.Background(), clientset, testNamespace), clientset)
tests := []struct {
name string
repoURL string
want *v1alpha1.Repository
wantErr bool
}{
{
name: "TestUnknownRepo",
repoURL: "http://unknown/repo",
wantErr: true,
},
{
name: "TestKnownRepo",
repoURL: "https://known/repo",
want: &v1alpha1.Repository{Repo: "https://known/repo"},
},
{
name: "TestSecuredRepo",
repoURL: "https://secured/repo",
want: &v1alpha1.Repository{Repo: "https://secured/repo", Username: "test-username", Password: "test-password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := db.GetRepository(context.TODO(), tt.repoURL)
if (err != nil) != tt.wantErr {
t.Errorf("db.GetHydratedRepository() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("db.GetHydratedRepository() = %v, want %v", got, tt.want)
}
})
}
}
func newManagedSecret() *v1.Secret {
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "managed-secret",
Namespace: testNamespace,
@@ -103,7 +148,21 @@ func TestDeleteRepositoryManagedSecrets(t *testing.T) {
username: []byte("test-username"),
password: []byte("test-password"),
},
})
}
}
func TestDeleteRepositoryManagedSecrets(t *testing.T) {
config := map[string]string{
"repositories": `
- url: https://github.com/argoproj/argocd-example-apps
usernameSecret:
name: managed-secret
key: username
passwordSecret:
name: managed-secret
key: password
`}
clientset := getClientset(config, newManagedSecret())
db := NewDB(testNamespace, settings.NewSettingsManager(context.Background(), clientset, testNamespace), clientset)
err := db.DeleteRepository(context.Background(), "https://github.com/argoproj/argocd-example-apps")

262
util/db/mocks/ArgoDB.go Normal file
View File

@@ -0,0 +1,262 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
package mocks
import context "context"
import db "github.com/argoproj/argo-cd/util/db"
import mock "github.com/stretchr/testify/mock"
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
// ArgoDB is an autogenerated mock type for the ArgoDB type
type ArgoDB struct {
mock.Mock
}
// CreateCluster provides a mock function with given fields: ctx, c
func (_m *ArgoDB) CreateCluster(ctx context.Context, c *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
ret := _m.Called(ctx, c)
var r0 *v1alpha1.Cluster
if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.Cluster) *v1alpha1.Cluster); ok {
r0 = rf(ctx, c)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Cluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1alpha1.Cluster) error); ok {
r1 = rf(ctx, c)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateRepository provides a mock function with given fields: ctx, r
func (_m *ArgoDB) CreateRepository(ctx context.Context, r *v1alpha1.Repository) (*v1alpha1.Repository, error) {
ret := _m.Called(ctx, r)
var r0 *v1alpha1.Repository
if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.Repository) *v1alpha1.Repository); ok {
r0 = rf(ctx, r)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Repository)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1alpha1.Repository) error); ok {
r1 = rf(ctx, r)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteCluster provides a mock function with given fields: ctx, name
func (_m *ArgoDB) DeleteCluster(ctx context.Context, name string) error {
ret := _m.Called(ctx, name)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteRepository provides a mock function with given fields: ctx, name
func (_m *ArgoDB) DeleteRepository(ctx context.Context, name string) error {
ret := _m.Called(ctx, name)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, name)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetCluster provides a mock function with given fields: ctx, name
func (_m *ArgoDB) GetCluster(ctx context.Context, name string) (*v1alpha1.Cluster, error) {
ret := _m.Called(ctx, name)
var r0 *v1alpha1.Cluster
if rf, ok := ret.Get(0).(func(context.Context, string) *v1alpha1.Cluster); ok {
r0 = rf(ctx, name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Cluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRepository provides a mock function with given fields: ctx, repoURL
func (_m *ArgoDB) GetRepository(ctx context.Context, repoURL string) (*v1alpha1.Repository, error) {
ret := _m.Called(ctx, repoURL)
var r0 *v1alpha1.Repository
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)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, repoURL)
} 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)
var r0 *v1alpha1.ClusterList
if rf, ok := ret.Get(0).(func(context.Context) *v1alpha1.ClusterList); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.ClusterList)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListHelmRepos provides a mock function with given fields: ctx
func (_m *ArgoDB) ListHelmRepos(ctx context.Context) ([]*v1alpha1.HelmRepository, error) {
ret := _m.Called(ctx)
var r0 []*v1alpha1.HelmRepository
if rf, ok := ret.Get(0).(func(context.Context) []*v1alpha1.HelmRepository); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*v1alpha1.HelmRepository)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListRepoURLs provides a mock function with given fields: ctx
func (_m *ArgoDB) ListRepoURLs(ctx context.Context) ([]string, error) {
ret := _m.Called(ctx)
var r0 []string
if rf, ok := ret.Get(0).(func(context.Context) []string); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateCluster provides a mock function with given fields: ctx, c
func (_m *ArgoDB) UpdateCluster(ctx context.Context, c *v1alpha1.Cluster) (*v1alpha1.Cluster, error) {
ret := _m.Called(ctx, c)
var r0 *v1alpha1.Cluster
if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.Cluster) *v1alpha1.Cluster); ok {
r0 = rf(ctx, c)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Cluster)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1alpha1.Cluster) error); ok {
r1 = rf(ctx, c)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateRepository provides a mock function with given fields: ctx, r
func (_m *ArgoDB) UpdateRepository(ctx context.Context, r *v1alpha1.Repository) (*v1alpha1.Repository, error) {
ret := _m.Called(ctx, r)
var r0 *v1alpha1.Repository
if rf, ok := ret.Get(0).(func(context.Context, *v1alpha1.Repository) *v1alpha1.Repository); ok {
r0 = rf(ctx, r)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Repository)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *v1alpha1.Repository) error); ok {
r1 = rf(ctx, r)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WatchClusters provides a mock function with given fields: ctx, callback
func (_m *ArgoDB) WatchClusters(ctx context.Context, callback func(*db.ClusterEvent)) error {
ret := _m.Called(ctx, callback)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, func(*db.ClusterEvent)) error); ok {
r0 = rf(ctx, callback)
} else {
r0 = ret.Error(0)
}
return r0
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/settings"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -45,7 +46,7 @@ func (db *db) CreateRepository(ctx context.Context, r *appsv1.Repository) (*apps
return nil, err
}
index := getRepoCredIndex(s, r.Repo)
index := getRepositoryIndex(s, r.Repo)
if index > -1 {
return nil, status.Errorf(codes.AlreadyExists, "repository '%s' already exists", r.Repo)
}
@@ -85,25 +86,47 @@ func (db *db) GetRepository(ctx context.Context, repoURL string) (*appsv1.Reposi
return nil, err
}
index := getRepoCredIndex(s, repoURL)
index := getRepositoryIndex(s, repoURL)
if index < 0 {
return nil, status.Errorf(codes.NotFound, "repo '%s' not found", repoURL)
}
repoInfo := s.Repositories[index]
repo, err := db.credentialsToRepository(s.Repositories[index])
if err != nil {
return nil, err
}
if !repo.HasCredentials() {
index := getRepositoryCredentialIndex(s, repoURL)
if index >= 0 {
credential, err := db.credentialsToRepository(s.RepositoryCredentials[index])
if err != nil {
return nil, err
} else {
log.WithFields(log.Fields{"repoURL": repo.Repo, "credUrl": credential.Repo}).Info("copying credentials")
repo.CopyCredentialsFrom(*credential)
}
}
}
return repo, err
}
func (db *db) credentialsToRepository(repoInfo settings.RepoCredentials) (*appsv1.Repository, error) {
repo := &appsv1.Repository{
Repo: repoInfo.URL,
InsecureIgnoreHostKey: repoInfo.InsecureIgnoreHostKey,
}
err = db.unmarshalFromSecretsStr(map[*string]*apiv1.SecretKeySelector{
err := db.unmarshalFromSecretsStr(map[*string]*apiv1.SecretKeySelector{
&repo.Username: repoInfo.UsernameSecret,
&repo.Password: repoInfo.PasswordSecret,
&repo.SSHPrivateKey: repoInfo.SSHPrivateKeySecret,
}, make(map[string]*apiv1.Secret))
if err != nil {
return nil, err
}
return repo, nil
return repo, err
}
// UpdateRepository updates a repository
@@ -113,7 +136,7 @@ func (db *db) UpdateRepository(ctx context.Context, r *appsv1.Repository) (*apps
return nil, err
}
index := getRepoCredIndex(s, r.Repo)
index := getRepositoryIndex(s, r.Repo)
if index < 0 {
return nil, status.Errorf(codes.NotFound, "repo '%s' not found", r.Repo)
}
@@ -138,7 +161,7 @@ func (db *db) DeleteRepository(ctx context.Context, repoURL string) error {
return err
}
index := getRepoCredIndex(s, repoURL)
index := getRepositoryIndex(s, repoURL)
if index < 0 {
return status.Errorf(codes.NotFound, "repo '%s' not found", repoURL)
}
@@ -243,9 +266,20 @@ func (db *db) upsertSecret(name string, data map[string][]byte) error {
return nil
}
func getRepoCredIndex(s *settings.ArgoCDSettings, repoURL string) int {
for i, cred := range s.Repositories {
if git.SameURL(cred.URL, repoURL) {
func getRepositoryIndex(s *settings.ArgoCDSettings, repoURL string) int {
for i, repo := range s.Repositories {
if git.SameURL(repo.URL, repoURL) {
return i
}
}
return -1
}
func getRepositoryCredentialIndex(s *settings.ArgoCDSettings, repoURL string) int {
repoURL = git.NormalizeGitURL(repoURL)
for i, cred := range s.RepositoryCredentials {
credUrl := git.NormalizeGitURL(cred.URL)
if strings.HasPrefix(repoURL, credUrl) {
return i
}
}

View File

@@ -1,6 +1,10 @@
package db
import "testing"
import (
"testing"
"github.com/argoproj/argo-cd/util/settings"
)
func TestRepoURLToSecretName(t *testing.T) {
tables := map[string]string{
@@ -18,3 +22,24 @@ func TestRepoURLToSecretName(t *testing.T) {
}
}
}
func Test_getRepositoryCredentialIndex(t *testing.T) {
argoCDSettings := settings.ArgoCDSettings{
RepositoryCredentials: []settings.RepoCredentials{{URL: "http://known"}},
}
tests := []struct {
name string
repoURL string
want int
}{
{"TestNotFound", "", -1},
{"TestFoundFound", "http://known/repo", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getRepositoryCredentialIndex(&argoCDSettings, tt.repoURL); got != tt.want {
t.Errorf("getRepositoryCredentialIndex() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -149,3 +149,12 @@ func TestTLS(address string) (*TLSTestResult, error) {
}
return nil, err
}
func WithTimeout(duration time.Duration) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
clientDeadline := time.Now().Add(duration)
ctx, cancel := context.WithDeadline(ctx, clientDeadline)
defer cancel()
return invoker(ctx, method, req, reply, cc, opts...)
}
}

View File

@@ -80,9 +80,11 @@ func filterAPIResources(config *rest.Config, resourceFilter ResourceFilter, filt
gv = schema.GroupVersion{}
}
for _, apiResource := range apiResourcesList.APIResources {
if resourceFilter.IsExcludedResource(gv.Group, apiResource.Kind, config.Host) {
continue
}
if _, ok := isObsoleteExtensionsGroupKind(gv.Group, apiResource.Kind); ok &&
// Edge case for deprecated Ingress kind.
!(gv.Group == "extensions" && apiResource.Kind == IngressKind && version.LessThan(ingressDeprecationVersion)) {

View File

@@ -30,4 +30,4 @@ func TestConvertToVersion(t *testing.T) {
gvk = newObj.GroupVersionKind()
assert.Equal(t, "apps", gvk.Group)
assert.Equal(t, "v1", gvk.Version)
}
}

View File

@@ -5,13 +5,13 @@ import (
log "github.com/sirupsen/logrus"
)
type ExcludedResource struct {
type FilteredResource struct {
APIGroups []string `json:"apiGroups,omitempty"`
Kinds []string `json:"kinds,omitempty"`
Clusters []string `json:"clusters,omitempty"`
}
func (r ExcludedResource) matchGroup(apiGroup string) bool {
func (r FilteredResource) matchGroup(apiGroup string) bool {
for _, excludedApiGroup := range r.APIGroups {
if match(excludedApiGroup, apiGroup) {
return true
@@ -29,7 +29,7 @@ func match(pattern, text string) bool {
return compiledGlob.Match(text)
}
func (r ExcludedResource) matchKind(kind string) bool {
func (r FilteredResource) matchKind(kind string) bool {
for _, excludedKind := range r.Kinds {
if excludedKind == "*" || excludedKind == kind {
return true
@@ -38,7 +38,7 @@ func (r ExcludedResource) matchKind(kind string) bool {
return len(r.Kinds) == 0
}
func (r ExcludedResource) matchCluster(cluster string) bool {
func (r FilteredResource) matchCluster(cluster string) bool {
for _, excludedCluster := range r.Clusters {
if match(excludedCluster, cluster) {
return true
@@ -47,6 +47,6 @@ func (r ExcludedResource) matchCluster(cluster string) bool {
return len(r.Clusters) == 0
}
func (r ExcludedResource) Match(apiGroup, kind, cluster string) bool {
func (r FilteredResource) Match(apiGroup, kind, cluster string) bool {
return r.matchGroup(apiGroup) && r.matchKind(kind) && r.matchCluster(cluster)
}

View File

@@ -12,28 +12,28 @@ func TestExcludeResource(t *testing.T) {
cluster := "baz.com"
// matches with missing values
assert.True(t, ExcludedResource{Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{"*"}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{"*"}}.Match(apiGroup, kind, cluster))
// simple matches
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{"*.com"}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{"*"}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{"*.com"}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{"*.com"}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{"*"}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{"*.com"}}.Match(apiGroup, kind, cluster))
// negative matches
assert.False(t, ExcludedResource{APIGroups: []string{""}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.False(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{""}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.False(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{""}}.Match(apiGroup, kind, cluster))
assert.False(t, FilteredResource{APIGroups: []string{""}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.False(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{""}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.False(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{""}}.Match(apiGroup, kind, cluster))
// complex matches
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup, apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{kind, kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, ExcludedResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster, cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup, apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{kind, kind}, Clusters: []string{cluster}}.Match(apiGroup, kind, cluster))
assert.True(t, FilteredResource{APIGroups: []string{apiGroup}, Kinds: []string{kind}, Clusters: []string{cluster, cluster}}.Match(apiGroup, kind, cluster))
// rubbish patterns
assert.False(t, ExcludedResource{APIGroups: []string{"["}, Kinds: []string{""}, Clusters: []string{""}}.Match("", "", ""))
assert.False(t, ExcludedResource{APIGroups: []string{""}, Kinds: []string{"["}, Clusters: []string{""}}.Match("", "", ""))
assert.False(t, ExcludedResource{APIGroups: []string{""}, Kinds: []string{""}, Clusters: []string{"["}}.Match("", "", ""))
assert.False(t, FilteredResource{APIGroups: []string{"["}, Kinds: []string{""}, Clusters: []string{""}}.Match("", "", ""))
assert.False(t, FilteredResource{APIGroups: []string{""}, Kinds: []string{"["}, Clusters: []string{""}}.Match("", "", ""))
assert.False(t, FilteredResource{APIGroups: []string{""}, Kinds: []string{""}, Clusters: []string{"["}}.Match("", "", ""))
}

View File

@@ -60,6 +60,8 @@ type ArgoCDSettings struct {
Secrets map[string]string `json:"secrets,omitempty"`
// Repositories holds list of configured git repositories
Repositories []RepoCredentials
// Repositories holds list of repo credentials
RepositoryCredentials []RepoCredentials
// Repositories holds list of configured helm repositories
HelmRepositories []HelmRepoCredentials
// AppInstanceLabelKey is the configured application instance label key used to label apps. May be empty
@@ -70,7 +72,9 @@ type ArgoCDSettings struct {
// (e.g. argoproj.io/rollout) for the resource that is being overridden
ResourceOverrides map[string]v1alpha1.ResourceOverride
// ResourceExclusions holds the api groups, kinds per cluster to exclude from Argo CD's watch
ResourceExclusions []ExcludedResource
ResourceExclusions []FilteredResource
// ResourceInclusions holds the only api groups, kinds per cluster that Argo CD will watch
ResourceInclusions []FilteredResource
}
type OIDCConfig struct {
@@ -114,6 +118,8 @@ const (
settingURLKey = "url"
// repositoriesKey designates the key where ArgoCDs repositories list is set
repositoriesKey = "repositories"
// repositoryCredentialsKey designates the key where ArgoCDs repositories credentials list is set
repositoryCredentialsKey = "repository.credentials"
// helmRepositoriesKey designates the key where list of helm repositories is set
helmRepositoriesKey = "helm.repositories"
// settingDexConfigKey designates the key for the dex config
@@ -132,6 +138,8 @@ const (
resourceCustomizationsKey = "resource.customizations"
// resourceExclusions is the key to the list of excluded resources
resourceExclusionsKey = "resource.exclusions"
// resourceInclusions is the key to the list of explicitly watched resources
resourceInclusionsKey = "resource.inclusions"
// configManagementPluginsKey is the key to the list of config management plugins
configManagementPluginsKey = "configManagementPlugins"
)
@@ -336,6 +344,7 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
settings.URL = argoCDCM.Data[settingURLKey]
settings.AppInstanceLabelKey = argoCDCM.Data[settingsApplicationInstanceLabelKey]
repositoriesStr := argoCDCM.Data[repositoriesKey]
repositoryCredentialsStr := argoCDCM.Data[repositoryCredentialsKey]
var errors []error
if repositoriesStr != "" {
repositories := make([]RepoCredentials, 0)
@@ -346,6 +355,15 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
settings.Repositories = repositories
}
}
if repositoryCredentialsStr != "" {
repositoryCredentials := make([]RepoCredentials, 0)
err := yaml.Unmarshal([]byte(repositoryCredentialsStr), &repositoryCredentials)
if err != nil {
errors = append(errors, err)
} else {
settings.RepositoryCredentials = repositoryCredentials
}
}
helmRepositoriesStr := argoCDCM.Data[helmRepositoriesKey]
if helmRepositoriesStr != "" {
helmRepositories := make([]HelmRepoCredentials, 0)
@@ -364,8 +382,18 @@ func updateSettingsFromConfigMap(settings *ArgoCDSettings, argoCDCM *apiv1.Confi
settings.ResourceOverrides = resourceOverrides
}
if value, ok := argoCDCM.Data[resourceInclusionsKey]; ok {
includedResources := make([]FilteredResource, 0)
err := yaml.Unmarshal([]byte(value), &includedResources)
if err != nil {
errors = append(errors, err)
} else {
settings.ResourceInclusions = includedResources
}
}
if value, ok := argoCDCM.Data[resourceExclusionsKey]; ok {
excludedResources := make([]ExcludedResource, 0)
excludedResources := make([]FilteredResource, 0)
err := yaml.Unmarshal([]byte(value), &excludedResources)
if err != nil {
errors = append(errors, err)
@@ -501,6 +529,15 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
} else {
delete(argoCDCM.Data, repositoriesKey)
}
if len(settings.RepositoryCredentials) > 0 {
yamlStr, err := yaml.Marshal(settings.RepositoryCredentials)
if err != nil {
return err
}
argoCDCM.Data[repositoryCredentialsKey] = string(yamlStr)
} else {
delete(argoCDCM.Data, repositoryCredentialsKey)
}
if settings.AppInstanceLabelKey != "" {
argoCDCM.Data[settingsApplicationInstanceLabelKey] = settings.AppInstanceLabelKey
} else {
@@ -517,6 +554,16 @@ func (mgr *SettingsManager) SaveSettings(settings *ArgoCDSettings) error {
delete(argoCDCM.Data, resourceCustomizationsKey)
}
if len(settings.ResourceInclusions) > 0 {
yamlBytes, err := yaml.Marshal(settings.ResourceInclusions)
if err != nil {
return err
}
argoCDCM.Data[resourceInclusionsKey] = string(yamlBytes)
} else {
delete(argoCDCM.Data, resourceInclusionsKey)
}
if len(settings.ResourceExclusions) > 0 {
yamlBytes, err := yaml.Marshal(settings.ResourceExclusions)
if err != nil {
@@ -837,21 +884,64 @@ func (a *ArgoCDSettings) GetAppInstanceLabelKey() string {
return a.AppInstanceLabelKey
}
func (a *ArgoCDSettings) getExcludedResources() []ExcludedResource {
coreExcludedResources := []ExcludedResource{
func (a *ArgoCDSettings) getExcludedResources() []FilteredResource {
coreExcludedResources := []FilteredResource{
{APIGroups: []string{"events.k8s.io", "metrics.k8s.io"}},
{APIGroups: []string{""}, Kinds: []string{"Event"}},
}
return append(coreExcludedResources, a.ResourceExclusions...)
}
func (a *ArgoCDSettings) IsExcludedResource(apiGroup, kind, cluster string) bool {
func (a *ArgoCDSettings) checkResourcePresence(apiGroup, kind, cluster string, filteredResources []FilteredResource) bool {
for _, excludedResource := range a.getExcludedResources() {
if excludedResource.Match(apiGroup, kind, cluster) {
for _, includedResource := range filteredResources {
if includedResource.Match(apiGroup, kind, cluster) {
return true
}
}
return false
}
func (a *ArgoCDSettings) isIncludedResource(apiGroup, kind, cluster string) bool {
return a.checkResourcePresence(apiGroup, kind, cluster, a.ResourceInclusions)
}
func (a *ArgoCDSettings) isExcludedResource(apiGroup, kind, cluster string) bool {
return a.checkResourcePresence(apiGroup, kind, cluster, a.getExcludedResources())
}
// Behavior of this function is as follows:
// +-------------+-------------+-------------+
// | Inclusions | Exclusions | Result |
// +-------------+-------------+-------------+
// | Empty | Empty | Allowed |
// +-------------+-------------+-------------+
// | Present | Empty | Allowed |
// +-------------+-------------+-------------+
// | Not Present | Empty | Not Allowed |
// +-------------+-------------+-------------+
// | Empty | Present | Not Allowed |
// +-------------+-------------+-------------+
// | Empty | Not Present | Allowed |
// +-------------+-------------+-------------+
// | Present | Not Present | Allowed |
// +-------------+-------------+-------------+
// | Not Present | Present | Not Allowed |
// +-------------+-------------+-------------+
// | Not Present | Not Present | Not Allowed |
// +-------------+-------------+-------------+
// | Present | Present | Not Allowed |
// +-------------+-------------+-------------+
//
func (a *ArgoCDSettings) IsExcludedResource(apiGroup, kind, cluster string) bool {
if len(a.ResourceInclusions) > 0 {
if a.isIncludedResource(apiGroup, kind, cluster) {
return a.isExcludedResource(apiGroup, kind, cluster)
} else {
return true
}
} else {
return a.isExcludedResource(apiGroup, kind, cluster)
}
}

View File

@@ -3,9 +3,8 @@ package settings
import (
"testing"
v1 "k8s.io/api/core/v1"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
)
func TestArgoCDSettings_IsExcludedResource(t *testing.T) {
@@ -15,7 +14,69 @@ func TestArgoCDSettings_IsExcludedResource(t *testing.T) {
assert.False(t, settings.IsExcludedResource("rubbish.io", "", ""))
}
func TestUpdateSettingsFromConfigMapResourceExclusions(t *testing.T) {
func Test_updateSettingsFromConfigMap(t *testing.T) {
tests := []struct {
name string
key string
value string
wantErr bool
get func(settings ArgoCDSettings) interface{}
want interface{}
}{
{
name: "TestResourceExclusions",
key: "resource.exclusions",
value: "\n - apiGroups: []\n kinds: []\n clusters: []\n",
get: func(settings ArgoCDSettings) interface{} {
return settings.ResourceExclusions
},
want: []FilteredResource{{APIGroups: []string{}, Kinds: []string{}, Clusters: []string{}}},
},
{
name: "TestResourceInclusion",
key: "resource.inclusions",
value: "\n - apiGroups: []\n kinds: [managed_only]\n clusters: []\n",
get: func(settings ArgoCDSettings) interface{} {
return settings.ResourceInclusions
},
want: []FilteredResource{{APIGroups: []string{}, Kinds: []string{"managed_only"}, Clusters: []string{}}},
},
{
name: "TestRepositories",
key: "repositories",
value: "\n - url: http://foo\n",
get: func(settings ArgoCDSettings) interface{} {
return settings.Repositories
},
want: []RepoCredentials{{URL: "http://foo"}},
},
{
name: "TestRepositoryCredentials",
key: "repository.credentials",
value: "\n - url: http://foo\n",
get: func(settings ArgoCDSettings) interface{} {
return settings.RepositoryCredentials
},
want: []RepoCredentials{{URL: "http://foo"}},
},
}
for _, tt := range tests {
settings := ArgoCDSettings{}
configMap := v1.ConfigMap{
Data: map[string]string{
tt.key: tt.value,
},
}
t.Run(tt.name, func(t *testing.T) {
if err := updateSettingsFromConfigMap(&settings, &configMap); (err != nil) != tt.wantErr {
t.Errorf("updateSettingsFromConfigMap() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, tt.want, tt.get(settings))
})
}
}
func TestResourceInclusions(t *testing.T) {
settings := ArgoCDSettings{}
configMap := v1.ConfigMap{}
@@ -25,11 +86,59 @@ func TestUpdateSettingsFromConfigMapResourceExclusions(t *testing.T) {
assert.Nil(t, settings.ResourceExclusions)
configMap.Data = map[string]string{
"resource.exclusions": "\n - apiGroups: []\n kinds: []\n clusters: []\n",
"resource.inclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: []\n clusters: []\n",
}
err = updateSettingsFromConfigMap(&settings, &configMap)
assert.NoError(t, err)
assert.Equal(t, []ExcludedResource{{APIGroups: []string{}, Kinds: []string{}, Clusters: []string{}}}, settings.ResourceExclusions)
assert.Equal(t, []FilteredResource{{APIGroups: []string{"whitelisted-resource"}, Kinds: []string{}, Clusters: []string{}}}, settings.ResourceInclusions)
assert.True(t, settings.IsExcludedResource("non-whitelisted-resource", "", ""))
assert.False(t, settings.IsExcludedResource("whitelisted-resource", "", ""))
}
func TestResourceInclusionsExclusionNonMutex(t *testing.T) {
settings := ArgoCDSettings{}
configMap := v1.ConfigMap{}
err := updateSettingsFromConfigMap(&settings, &configMap)
assert.NoError(t, err)
assert.Nil(t, settings.ResourceExclusions)
configMap.Data = map[string]string{
"resource.inclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: []\n clusters: []\n",
"resource.exclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: [\"blacklisted-kind\"]\n clusters: []\n",
}
err = updateSettingsFromConfigMap(&settings, &configMap)
assert.NoError(t, err)
assert.True(t, settings.IsExcludedResource("whitelisted-resource", "blacklisted-kind", ""))
assert.False(t, settings.IsExcludedResource("whitelisted-resource", "", ""))
assert.False(t, settings.IsExcludedResource("whitelisted-resource", "non-blacklisted-kind", ""))
configMap.Data = map[string]string{
"resource.inclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: [\"whitelisted-kind\"]\n clusters: []\n",
"resource.exclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: []\n clusters: []\n",
}
err = updateSettingsFromConfigMap(&settings, &configMap)
assert.NoError(t, err)
assert.True(t, settings.IsExcludedResource("whitelisted-resource", "whitelisted-kind", ""))
assert.True(t, settings.IsExcludedResource("whitelisted-resource", "", ""))
assert.True(t, settings.IsExcludedResource("whitelisted-resource", "non-whitelisted-kind", ""))
configMap.Data = map[string]string{
"resource.inclusions": "\n - apiGroups: [\"foo-bar\"]\n kinds: [\"whitelisted-kind\"]\n clusters: []\n",
"resource.exclusions": "\n - apiGroups: [\"whitelisted-resource\"]\n kinds: []\n clusters: []\n",
}
err = updateSettingsFromConfigMap(&settings, &configMap)
assert.NoError(t, err)
assert.True(t, settings.IsExcludedResource("not-whitelisted-resource", "whitelisted-kind", ""))
assert.True(t, settings.IsExcludedResource("not-whitelisted-resource", "", ""))
}