Repository: grafana/tanka (go)

Issue:
`tk apply` diff error UX improvements
When running a `tk apply`, if a diff error is encountered, the command will still prompt the user to "Please type 'yes' to confirm:". As a user, I would prefer not to apply changes unless I have complete information and feel that a diff failure should be notably different to the warning of an empty diff.

It would also be nice to provide a way of avoiding diff issues in some way. For instance, a common source of diff errors is admission webhooks that do not support dry run applies. In these circumstances the entire diff fails and it would be nice for the user to be able specify a way of ignoring the failing resources during the diff stage. Perhaps an inverse of the `-t, --target` flag.


Pull request description:
feat(cli): support negative filters
Output filtering (`-t`) now supports excluding objects, by prepending an
exlamation mark (`!`) before the regular expression

Fixes #324 

Patch:
diff --git a/docs/docs/targets.md b/docs/docs/targets.md
index 6b8edb25..e8fdb270 100644
--- a/docs/docs/targets.md
+++ b/docs/docs/targets.md
@@ -78,3 +78,14 @@ apiVersion: apps/v1
 kind: Deployment
 # ...
 ```
+
+## Excluding
+
+Sometimes it may be desirably to exclude a single object, instead of including all others.
+
+To do so, prepend the regular expression with an exclamation mark (`!`), like so:
+
+```bash
+# filter out all Deployments
+$ tk show . -t '!deployment/.*'
+```
diff --git a/pkg/process/filter.go b/pkg/process/filter.go
index 8e8d7e79..6b2b8c5d 100644
--- a/pkg/process/filter.go
+++ b/pkg/process/filter.go
@@ -9,12 +9,16 @@ import (
 )
 
 // Filter returns all elements of the list that match at least one expression
+// and are not ignored
 func Filter(list manifest.List, exprs Matchers) manifest.List {
 	out := make(manifest.List, 0, len(list))
 	for _, m := range list {
 		if !exprs.MatchString(m.KindName()) {
 			continue
 		}
+		if exprs.IgnoreString(m.KindName()) {
+			continue
+		}
 		out = append(out, m)
 	}
 	return out
@@ -26,7 +30,13 @@ type Matcher interface {
 	MatchString(string) bool
 }
 
+// Ignorer is like matcher, but for explicitely ignoring resources
+type Ignorer interface {
+	IgnoreString(string) bool
+}
+
 // Matchers is a collection of multiple expressions.
+// A matcher may also implement Ignorer to explicitely ignore fields
 type Matchers []Matcher
 
 // MatchString returns whether at least one expression (OR) matches the string
@@ -38,6 +48,18 @@ func (e Matchers) MatchString(s string) bool {
 	return b
 }
 
+func (e Matchers) IgnoreString(s string) bool {
+	b := false
+	for _, exp := range e {
+		i, ok := exp.(Ignorer)
+		if !ok {
+			continue
+		}
+		b = b || i.IgnoreString(s)
+	}
+	return b
+}
+
 // RegExps is a helper to construct Matchers from regular expressions
 func RegExps(rs []*regexp.Regexp) Matchers {
 	xprs := make(Matchers, 0, len(rs))
@@ -50,11 +72,20 @@ func RegExps(rs []*regexp.Regexp) Matchers {
 func StrExps(strs ...string) (Matchers, error) {
 	exps := make(Matchers, 0, len(strs))
 	for _, raw := range strs {
-		s := fmt.Sprintf(`(?i)^%s$`, raw)
+		// trim exlamation mark, not supported by regex
+		s := fmt.Sprintf(`(?i)^%s$`, strings.TrimPrefix(raw, "!"))
+
+		// create regexp matcher
+		var exp Matcher
 		exp, err := regexp.Compile(s)
 		if err != nil {
 			return nil, ErrBadExpr{err}
 		}
+
+		// if negative (!), invert regex behaviour
+		if strings.HasPrefix(raw, "!") {
+			exp = NegMatcher{exp: exp}
+		}
 		exps = append(exps, exp)
 	}
 	return exps, nil
@@ -76,3 +107,16 @@ type ErrBadExpr struct {
 func (e ErrBadExpr) Error() string {
 	return fmt.Sprintf("%s.\nSee https://tanka.dev/output-filtering/#regular-expressions for details on regular expressions.", strings.Title(e.inner.Error()))
 }
+
+// NexMatcher is a matcher that inverts the original behaviour
+type NegMatcher struct {
+	exp Matcher
+}
+
+func (n NegMatcher) MatchString(s string) bool {
+	return true
+}
+
+func (n NegMatcher) IgnoreString(s string) bool {
+	return n.exp.MatchString(s)
+}


Test patch:
diff --git a/pkg/process/process_test.go b/pkg/process/process_test.go
index 5014b380..cf046159 100644
--- a/pkg/process/process_test.go
+++ b/pkg/process/process_test.go
@@ -64,6 +64,15 @@ func TestProcess(t *testing.T) {
 				`DePlOyMeNt/GrAfAnA`,
 			),
 		},
+		{
+			name: "targets-negative",
+			deep: testDataDeep().Deep,
+			flat: manifest.List{
+				testDataDeep().Flat[".app.web.frontend.nodejs.express.service"],
+				testDataDeep().Flat[".app.namespace"],
+			},
+			targets: MustStrExps(`!deployment/.*`),
+		},
 		{
 			name: "unwrap-list",
 			deep: loadFixture("list").Deep,