diff --git a/_examples/monorepo/squadrons/checkout/backend/squadron.yaml b/_examples/monorepo/squadrons/checkout/backend/squadron.yaml index df96fcd..7bebf5c 100644 --- a/_examples/monorepo/squadrons/checkout/backend/squadron.yaml +++ b/_examples/monorepo/squadrons/checkout/backend/squadron.yaml @@ -5,6 +5,7 @@ squadron: checkout: backend: chart: <% env "PROJECT_ROOT" %>/../common/charts/backend + tags: [ "backend" ] builds: default: tag: <% .Global.docker.tag | quote %> diff --git a/_examples/monorepo/squadrons/checkout/frontend/squadron.yaml b/_examples/monorepo/squadrons/checkout/frontend/squadron.yaml index dca1fc0..998cde1 100644 --- a/_examples/monorepo/squadrons/checkout/frontend/squadron.yaml +++ b/_examples/monorepo/squadrons/checkout/frontend/squadron.yaml @@ -5,6 +5,7 @@ squadron: checkout: frontend: chart: <% env "PROJECT_ROOT" %>/../common/charts/frontend + tags: [ "frontend" ] builds: default: tag: "<% .Global.docker.tag %>" diff --git a/_examples/monorepo/squadrons/storefinder/backend/squadron.yaml b/_examples/monorepo/squadrons/storefinder/backend/squadron.yaml index 511e809..444f250 100644 --- a/_examples/monorepo/squadrons/storefinder/backend/squadron.yaml +++ b/_examples/monorepo/squadrons/storefinder/backend/squadron.yaml @@ -5,6 +5,7 @@ squadron: storefinder: backend: chart: <% env "PROJECT_ROOT" %>/../common/charts/backend + tags: [ "backend" ] builds: default: tag: <% .Global.docker.tag | quote %> diff --git a/_examples/monorepo/squadrons/storefinder/frontend/squadron.yaml b/_examples/monorepo/squadrons/storefinder/frontend/squadron.yaml index 6eb6297..d211efe 100644 --- a/_examples/monorepo/squadrons/storefinder/frontend/squadron.yaml +++ b/_examples/monorepo/squadrons/storefinder/frontend/squadron.yaml @@ -5,6 +5,7 @@ squadron: storefinder: frontend: chart: <% env "PROJECT_ROOT" %>/../common/charts/frontend + tags: [ "frontend" ] builds: default: tag: "<% .Global.docker.tag %>" diff --git a/cmd/actions/build.go b/cmd/actions/build.go index 9016f31..6e8d790 100644 --- a/cmd/actions/build.go +++ b/cmd/actions/build.go @@ -8,6 +8,7 @@ import ( func init() { buildCmd.Flags().BoolVarP(&flagPush, "push", "p", false, "pushes built squadron units to the registry") buildCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") + buildCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") buildCmd.Flags().StringSliceVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args") buildCmd.Flags().StringSliceVar(&flagPushArgs, "push-args", nil, "additional docker push args") } @@ -25,7 +26,7 @@ var buildCmd = &cobra.Command{ } squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/config.go b/cmd/actions/config.go index 5b6784f..bebf3dd 100644 --- a/cmd/actions/config.go +++ b/cmd/actions/config.go @@ -3,14 +3,14 @@ package actions import ( "fmt" + "github.com/foomo/squadron" "github.com/foomo/squadron/internal/util" "github.com/spf13/cobra" - - "github.com/foomo/squadron" ) func init() { configCmd.Flags().BoolVar(&flagNoRender, "no-render", false, "don't render the config template") + configCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var configCmd = &cobra.Command{ @@ -26,7 +26,7 @@ var configCmd = &cobra.Command{ } squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/diff.go b/cmd/actions/diff.go index 8b7ec17..aad360d 100644 --- a/cmd/actions/diff.go +++ b/cmd/actions/diff.go @@ -7,7 +7,7 @@ import ( ) func init() { - diffCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + diffCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") diffCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") } diff --git a/cmd/actions/down.go b/cmd/actions/down.go index 34c56ba..2ab1813 100644 --- a/cmd/actions/down.go +++ b/cmd/actions/down.go @@ -8,7 +8,8 @@ import ( func init() { downCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") - downCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "Specifies the namespace") + downCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") + downCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var downCmd = &cobra.Command{ @@ -26,7 +27,7 @@ var downCmd = &cobra.Command{ args, helmArgs := parseExtraArgs(args) squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/list.go b/cmd/actions/list.go index 28e70b9..59cbc24 100644 --- a/cmd/actions/list.go +++ b/cmd/actions/list.go @@ -10,6 +10,10 @@ import ( var flagPrefixSquadron bool +func init() { + listCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") +} + var listCmd = &cobra.Command{ Use: "list [SQUADRON]", Short: "list squadron units", @@ -23,7 +27,7 @@ var listCmd = &cobra.Command{ } squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/push.go b/cmd/actions/push.go index 10627f1..41e48f8 100644 --- a/cmd/actions/push.go +++ b/cmd/actions/push.go @@ -6,11 +6,12 @@ import ( ) func init() { - pushCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + pushCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") pushCmd.Flags().BoolVarP(&flagBuild, "build", "b", false, "builds or rebuilds units") pushCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") pushCmd.Flags().StringSliceVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args") pushCmd.Flags().StringSliceVar(&flagPushArgs, "push-args", nil, "additional docker push args") + pushCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var pushCmd = &cobra.Command{ @@ -25,7 +26,7 @@ var pushCmd = &cobra.Command{ } squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/rollback.go b/cmd/actions/rollback.go index 9b6fb90..aa333fb 100644 --- a/cmd/actions/rollback.go +++ b/cmd/actions/rollback.go @@ -8,8 +8,9 @@ import ( func init() { rollbackCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") - rollbackCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + rollbackCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") rollbackCmd.Flags().StringVarP(&flagRevision, "revision", "r", "", "specifies the revision to roll back to") + rollbackCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var rollbackCmd = &cobra.Command{ @@ -26,7 +27,7 @@ var rollbackCmd = &cobra.Command{ args, helmArgs := parseExtraArgs(args) squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/root.go b/cmd/actions/root.go index 9049b1d..7bfd6ed 100644 --- a/cmd/actions/root.go +++ b/cmd/actions/root.go @@ -46,6 +46,7 @@ var ( flagParallel int flagBuildArgs []string flagPushArgs []string + flagTags []string flagDiff bool flagFiles []string ) diff --git a/cmd/actions/status.go b/cmd/actions/status.go index 1ca4b2d..d83e47e 100644 --- a/cmd/actions/status.go +++ b/cmd/actions/status.go @@ -8,7 +8,8 @@ import ( func init() { statusCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") - statusCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + statusCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") + statusCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var statusCmd = &cobra.Command{ @@ -25,7 +26,7 @@ var statusCmd = &cobra.Command{ args, helmArgs := parseExtraArgs(args) squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/template.go b/cmd/actions/template.go index 4ccd753..e411c2d 100644 --- a/cmd/actions/template.go +++ b/cmd/actions/template.go @@ -9,7 +9,8 @@ import ( ) func init() { - templateCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + templateCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") + templateCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var templateCmd = &cobra.Command{ @@ -27,7 +28,7 @@ var templateCmd = &cobra.Command{ args, helmArgs := parseExtraArgs(args) squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/cmd/actions/up.go b/cmd/actions/up.go index d0e1d78..27c36ff 100644 --- a/cmd/actions/up.go +++ b/cmd/actions/up.go @@ -10,12 +10,13 @@ import ( ) func init() { - upCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "specifies the namespace") + upCmd.Flags().StringVarP(&flagNamespace, "namespace", "n", "default", "set the namespace name or template (default, squadron-{{.Squadron}}-{{.Unit}})") upCmd.Flags().BoolVarP(&flagBuild, "build", "b", false, "builds or rebuilds units") upCmd.Flags().BoolVarP(&flagPush, "push", "p", false, "pushes units to the registry") upCmd.Flags().IntVar(&flagParallel, "parallel", 1, "run command in parallel") upCmd.Flags().StringSliceVar(&flagBuildArgs, "build-args", nil, "additional docker buildx build args") upCmd.Flags().StringSliceVar(&flagPushArgs, "push-args", nil, "additional docker push args") + upCmd.Flags().StringSliceVar(&flagTags, "tags", nil, "list of tags to include or exclude (can specify multiple or separate values with commas: tag1,tag2,-tag3)") } var upCmd = &cobra.Command{ @@ -32,7 +33,7 @@ var upCmd = &cobra.Command{ args, helmArgs := parseExtraArgs(args) squadronName, unitNames := parseSquadronAndUnitNames(args) - if err := sq.FilterConfig(squadronName, unitNames); err != nil { + if err := sq.FilterConfig(squadronName, unitNames, flagTags); err != nil { return err } diff --git a/internal/config/build.go b/internal/config/build.go index d91625c..1128cff 100644 --- a/internal/config/build.go +++ b/internal/config/build.go @@ -4,10 +4,9 @@ import ( "context" "fmt" + "github.com/foomo/squadron/internal/util" "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" - - "github.com/foomo/squadron/internal/util" ) type Build struct { diff --git a/internal/config/map.go b/internal/config/map.go index 6b3e71c..9b0c0e5 100644 --- a/internal/config/map.go +++ b/internal/config/map.go @@ -80,6 +80,15 @@ func (m Map[T]) Filter(keys ...string) error { return nil } +func (m Map[T]) FilterFn(handler func(key string, value T) bool) error { + for key, value := range m { + if !handler(key, value) { + delete(m, key) + } + } + return nil +} + func (m Map[T]) Iterate(handler func(key string, value T) error) error { if len(m) == 0 { return nil diff --git a/internal/config/tag.go b/internal/config/tag.go new file mode 100644 index 0000000..5493bd5 --- /dev/null +++ b/internal/config/tag.go @@ -0,0 +1,3 @@ +package config + +type Tag string diff --git a/internal/config/unit.go b/internal/config/unit.go index 78c1cf7..4b5ee9b 100644 --- a/internal/config/unit.go +++ b/internal/config/unit.go @@ -14,6 +14,7 @@ import ( type Unit struct { Chart helm.Dependency `yaml:"chart,omitempty"` + Tags []Tag `yaml:"tags,omitempty"` Builds map[string]Build `yaml:"builds,omitempty"` Values map[string]interface{} `yaml:"values,omitempty"` } diff --git a/squadron.go b/squadron.go index 20b67ab..bbfa986 100644 --- a/squadron.go +++ b/squadron.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "slices" "strings" "text/template" @@ -102,13 +103,11 @@ func (sq *Squadron) MergeConfigFiles() error { return nil } -func (sq *Squadron) FilterConfig(squadron string, units []string) error { - if len(squadron) == 0 { - return nil - } - - if err := sq.Config().Squadrons.Filter(squadron); err != nil { - return err +func (sq *Squadron) FilterConfig(squadron string, units, tags []string) error { + if len(squadron) > 0 { + if err := sq.Config().Squadrons.Filter(squadron); err != nil { + return err + } } if len(squadron) > 0 && len(units) > 0 { @@ -117,6 +116,27 @@ func (sq *Squadron) FilterConfig(squadron string, units []string) error { } } + if len(tags) > 0 { + if err := sq.Config().Squadrons.Iterate(func(key string, value config.Map[*config.Unit]) error { + return value.FilterFn(func(k string, v *config.Unit) bool { + for _, tag := range tags { + if strings.HasPrefix(tag, "-") { + if slices.Contains(v.Tags, config.Tag(strings.TrimPrefix(tag, "-"))) { + return false + } + } else if !slices.Contains(v.Tags, config.Tag(tag)) { + return false + } + } + return true + }) + }); err != nil { + return err + } + } + + sq.c.Trim() + value, err := yamlv2.Marshal(sq.c) if err != nil { return err diff --git a/squadron_test.go b/squadron_test.go index 6fd48cc..f50cdad 100644 --- a/squadron_test.go +++ b/squadron_test.go @@ -9,15 +9,20 @@ import ( "github.com/foomo/squadron" "github.com/foomo/squadron/internal/testutils" "github.com/foomo/squadron/internal/util" + "github.com/pterm/pterm" "github.com/stretchr/testify/require" ) func TestConfigSimpleSnapshot(t *testing.T) { + pterm.EnableDebugMessages() + require.NoError(t, os.Setenv("PROJECT_ROOT", ".")) + tests := []struct { name string files []string squadron string units []string + tags []string }{ { name: "blank", @@ -39,21 +44,25 @@ func TestConfigSimpleSnapshot(t *testing.T) { name: "template", files: []string{"squadron.yaml"}, }, + { + name: "tags", + tags: []string{"backend", "-skip"}, + files: []string{"squadron.yaml"}, + }, } for _, test := range tests { t.Run(test.name, func(tt *testing.T) { - config(tt, test.name, test.files, test.squadron, test.units) + config(tt, test.name, test.files, test.squadron, test.units, test.tags) }) } } -func config(t *testing.T, name string, files []string, squadronName string, unitNames []string) { +func config(t *testing.T, name string, files []string, squadronName string, unitNames, tags []string) { t.Helper() var cwd string ctx := context.TODO() require.NoError(t, util.ValidatePath(".", &cwd)) - require.NoError(t, os.Setenv("PROJECT_ROOT", ".")) for i, file := range files { files[i] = path.Join("testdata", name, file) @@ -65,7 +74,7 @@ func config(t *testing.T, name string, files []string, squadronName string, unit } { - require.NoError(t, sq.FilterConfig(squadronName, unitNames), "failed to filter config") + require.NoError(t, sq.FilterConfig(squadronName, unitNames, tags), "failed to filter config") testutils.Snapshot(t, path.Join("testdata", name, "snapshop-config-norender.yaml"), sq.ConfigYAML()) } diff --git a/testdata/tags/snapshop-config-norender.yaml b/testdata/tags/snapshop-config-norender.yaml new file mode 100644 index 0000000..4242767 --- /dev/null +++ b/testdata/tags/snapshop-config-norender.yaml @@ -0,0 +1,14 @@ +version: "2.0" +squadron: + checkout: + backend: + chart: + name: backend + repository: file://<% env "PROJECT_ROOT" %>/_examples/common/charts/backend + version: 0.0.1 + tags: + - backend + values: + image: + repository: nginx + tag: latest diff --git a/testdata/tags/snapshop-config.yaml b/testdata/tags/snapshop-config.yaml new file mode 100644 index 0000000..8627480 --- /dev/null +++ b/testdata/tags/snapshop-config.yaml @@ -0,0 +1,14 @@ +version: "2.0" +squadron: + checkout: + backend: + chart: + name: backend + repository: file://./_examples/common/charts/backend + version: 0.0.1 + tags: + - backend + values: + image: + repository: nginx + tag: latest diff --git a/testdata/tags/snapshop-template.yaml b/testdata/tags/snapshop-template.yaml new file mode 100644 index 0000000..16d3567 --- /dev/null +++ b/testdata/tags/snapshop-template.yaml @@ -0,0 +1,49 @@ +--- +# Source: backend/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/component: backend + app.kubernetes.io/managed-by: Helm + helm.sh/chart: backend-0.0.1 +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: backend + app.kubernetes.io/component: backend + ports: + - name: http + port: 80 +--- +# Source: backend/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/component: backend + app.kubernetes.io/managed-by: Helm + helm.sh/chart: backend-0.0.1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: backend + app.kubernetes.io/component: backend + template: + metadata: + labels: + app.kubernetes.io/name: backend + app.kubernetes.io/component: backend + spec: + containers: + - name: backend + image: nginx:latest + ports: + - name: http + protocol: TCP + containerPort: 80 diff --git a/testdata/tags/squadron.yaml b/testdata/tags/squadron.yaml new file mode 100644 index 0000000..4a95ee3 --- /dev/null +++ b/testdata/tags/squadron.yaml @@ -0,0 +1,33 @@ +version: "2.0" + +squadron: + storefinder: + frontend: + chart: <% env "PROJECT_ROOT" %>/_examples/common/charts/frontend + tags: ["frontend"] + values: + image: + tag: latest + repository: nginx + backend: + chart: <% env "PROJECT_ROOT" %>/_examples/common/charts/backend + tags: ["backend", "skip"] + values: + image: + tag: latest + repository: nginx + checkout: + frontend: + chart: <% env "PROJECT_ROOT" %>/_examples/common/charts/frontend + tags: ["frontend"] + values: + image: + tag: latest + repository: nginx + backend: + chart: <% env "PROJECT_ROOT" %>/_examples/common/charts/backend + tags: ["backend"] + values: + image: + tag: latest + repository: nginx