diff --git a/pkg/app/compose.go b/pkg/app/compose.go index 82066c4c..f8f6db51 100644 --- a/pkg/app/compose.go +++ b/pkg/app/compose.go @@ -10,18 +10,60 @@ import ( composetypes "github.com/docker/cli/cli/compose/types" ) -// SetRecipeLabel adds the label 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' to the app container -// to signal which recipe is connected to the deployed app -func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) { - for _, service := range compose.Services { - if service.Name == "app" { - log.Debug(i18n.G("set recipe label 'coop-cloud.%s.recipe' to %s for %s", stackName, recipe, stackName)) - labelKey := fmt.Sprintf("coop-cloud.%s.recipe", stackName) - service.Deploy.Labels[labelKey] = recipe +// setLabel writes a label to both the service object and the task template of +// every service of the stack. +// +// The task template labels end up on the containers themselves and are therefore +// visible to container-level tooling (cAdvisor, log shippers, docker events), +// which cannot read service labels at all. +// +// The service object is written as well because it is free: changing +// Spec.Labels updates the service without recreating any task and because +// `docker service ls --filter label=...` matches service labels only. +func setLabel(compose *composetypes.Config, key string, value string) { + for i := range compose.Services { + if compose.Services[i].Deploy.Labels == nil { + compose.Services[i].Deploy.Labels = composetypes.Labels{} } + + if compose.Services[i].Labels == nil { + compose.Services[i].Labels = composetypes.Labels{} + } + + compose.Services[i].Deploy.Labels[key] = value + compose.Services[i].Labels[key] = value } } +// setAppLabel writes a label to the service object of the app service only. +func setAppLabel(compose *composetypes.Config, key string, value string) { + for i := range compose.Services { + if compose.Services[i].Name != "app" { + continue + } + + if compose.Services[i].Deploy.Labels == nil { + compose.Services[i].Deploy.Labels = composetypes.Labels{} + } + + compose.Services[i].Deploy.Labels[key] = value + } +} + +// SetRecipeLabel adds two distinct labels to signal which recipe is connected +// to the deployed app: +// - 'coop-cloud.${STACK_NAME}.recipe=${RECIPE}' on the app service object, +// unchanged, as read back by GetLabel +// - 'coop-cloud.recipe=${RECIPE}' on every service of the stack, so that +// tooling can attribute any container to a recipe without knowing the +// stack name up front +func SetRecipeLabel(compose *composetypes.Config, stackName string, recipe string) { + log.Debug(i18n.G("set recipe labels 'coop-cloud.%s.recipe' and 'coop-cloud.recipe' to %s for %s", stackName, recipe, stackName)) + + setAppLabel(compose, fmt.Sprintf("coop-cloud.%s.recipe", stackName), recipe) + setLabel(compose, "coop-cloud.recipe", recipe) +} + // SetChaosLabel adds the label 'coop-cloud.${STACK_NAME}.chaos=true/false' to the app container // to signal if the app is deployed in chaos mode func SetChaosLabel(compose *composetypes.Config, stackName string, chaos bool) { diff --git a/pkg/app/compose_test.go b/pkg/app/compose_test.go index 81eeee36..d8a6b025 100644 --- a/pkg/app/compose_test.go +++ b/pkg/app/compose_test.go @@ -8,6 +8,7 @@ import ( testPkg "coopcloud.tech/abra/pkg/test" stack "coopcloud.tech/abra/pkg/upstream/stack" + composetypes "github.com/docker/cli/cli/compose/types" "github.com/stretchr/testify/assert" ) @@ -61,3 +62,43 @@ func TestGetTimeoutFromLabel(t *testing.T) { assert.Equal(t, timeout, test.expectedTimeout) } } + +func TestSetRecipeLabel(t *testing.T) { + // the app service brings labels along on both levels, the db service + // has none at all, so the pre-existing and the nil map case are covered + compose := &composetypes.Config{ + Services: []composetypes.ServiceConfig{ + { + Name: "app", + Labels: composetypes.Labels{"example.container.label": "keep me"}, + Deploy: composetypes.DeployConfig{ + Labels: composetypes.Labels{"coop-cloud.backupbot.enabled": "true"}, + }, + }, + {Name: "db"}, + }, + } + + appPkg.SetRecipeLabel(compose, "test_example_com", "test-recipe") + + services := make(map[string]composetypes.ServiceConfig) + for _, service := range compose.Services { + services[service.Name] = service + } + + // the stack scoped label stays on the app service object only + assert.Equal(t, "test-recipe", + services["app"].Deploy.Labels["coop-cloud.test_example_com.recipe"]) + assert.NotContains(t, services["db"].Deploy.Labels, + "coop-cloud.test_example_com.recipe") + + // the static label reaches every container of the stack + for name, service := range services { + assert.Equal(t, "test-recipe", service.Labels["coop-cloud.recipe"], name) + assert.Equal(t, "test-recipe", service.Deploy.Labels["coop-cloud.recipe"], name) + } + + // labels the recipe brought along are left alone + assert.Equal(t, "keep me", services["app"].Labels["example.container.label"]) + assert.Equal(t, "true", services["app"].Deploy.Labels["coop-cloud.backupbot.enabled"]) +} diff --git a/pkg/i18n/locales/abra.pot b/pkg/i18n/locales/abra.pot index b4a50a2f..845c855f 100644 --- a/pkg/i18n/locales/abra.pot +++ b/pkg/i18n/locales/abra.pot @@ -3047,7 +3047,7 @@ msgstr "" msgid "generated secrets %s shown again, please take note of them %s" msgstr "" -#: ./pkg/app/compose.go:63 +#: ./pkg/app/compose.go:105 #, c-format msgid "get label '%s'" msgstr "" @@ -3695,7 +3695,7 @@ msgstr "" msgid "no %s exists, skipping reading gitignore paths" msgstr "" -#: ./pkg/app/compose.go:69 +#: ./pkg/app/compose.go:111 #, c-format msgid "no %s label found for %s" msgstr "" @@ -4746,24 +4746,24 @@ msgstr "" msgid "set 'main' as the default branch" msgstr "" -#: ./pkg/app/compose.go:30 +#: ./pkg/app/compose.go:72 #, c-format msgid "set label 'coop-cloud.%s.chaos' to %v for %s" msgstr "" -#: ./pkg/app/compose.go:41 +#: ./pkg/app/compose.go:83 #, c-format msgid "set label 'coop-cloud.%s.chaos-version' to %v for %s" msgstr "" -#: ./pkg/app/compose.go:51 +#: ./pkg/app/compose.go:93 #, c-format msgid "set label 'coop-cloud.%s.version' to %v for %s" msgstr "" -#: ./pkg/app/compose.go:18 +#: ./pkg/app/compose.go:61 #, c-format -msgid "set recipe label 'coop-cloud.%s.recipe' to %s for %s" +msgid "set recipe labels 'coop-cloud.%s.recipe' and 'coop-cloud.recipe' to %s for %s" msgstr "" #: ./pkg/git/init.go:60 @@ -5064,7 +5064,7 @@ msgstr "" msgid "timed out on undeploy (timeout=%v sec)" msgstr "" -#: ./pkg/app/compose.go:80 +#: ./pkg/app/compose.go:122 #, c-format msgid "timeout label: %s" msgstr "" @@ -5172,7 +5172,7 @@ msgstr "" msgid "unable to continue, input required for initial version" msgstr "" -#: ./pkg/app/compose.go:85 +#: ./pkg/app/compose.go:127 #, c-format msgid "unable to convert timeout label %s to int: %s" msgstr ""