From 1df129615f697ff6351e1807bb90437f9cecf8fa Mon Sep 17 00:00:00 2001 From: Solly Ross Date: Wed, 7 Feb 2018 17:00:41 -0500 Subject: [PATCH] Add migrate command for legacy HPAs There are current broken HPAs floating around that either use the legacy oapi DeploymentConfig defintion (`v1.DeploymentConfig`) or the incorrect API group, due to the webconsole (all scalables, but with the group as `extensions/v1beta1`). This introduces a migrate command that corrects the ScaleTargetRef of those HPAs to have correct API groups that are usable by generic scale clients. --- contrib/completions/bash/oc | 86 +++++++++ contrib/completions/zsh/oc | 86 +++++++++ docs/man/man1/.files_generated_oc | 1 + docs/man/man1/oc-adm-migrate-legacy-hpa.1 | 3 + pkg/oc/admin/admin.go | 2 + pkg/oc/admin/migrate/legacyhpa/hpa.go | 190 +++++++++++++++++++ pkg/oc/admin/migrate/legacyhpa/hpa_test.go | 93 +++++++++ test/cmd/migrate.sh | 10 + test/testdata/hpa/legacy-and-normal-hpa.yaml | 22 +++ 9 files changed, 493 insertions(+) create mode 100644 docs/man/man1/oc-adm-migrate-legacy-hpa.1 create mode 100644 pkg/oc/admin/migrate/legacyhpa/hpa.go create mode 100644 pkg/oc/admin/migrate/legacyhpa/hpa_test.go create mode 100644 test/testdata/hpa/legacy-and-normal-hpa.yaml diff --git a/contrib/completions/bash/oc b/contrib/completions/bash/oc index 0c43c136f413..97d634d11867 100644 --- a/contrib/completions/bash/oc +++ b/contrib/completions/bash/oc @@ -4444,6 +4444,91 @@ _oc_adm_migrate_image-references() noun_aliases=() } +_oc_adm_migrate_legacy-hpa() +{ + last_command="oc_adm_migrate_legacy-hpa" + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--all-namespaces") + local_nonpersistent_flags+=("--all-namespaces") + flags+=("--allow-missing-template-keys") + local_nonpersistent_flags+=("--allow-missing-template-keys") + flags+=("--confirm") + local_nonpersistent_flags+=("--confirm") + flags+=("--filename=") + flags_with_completion+=("--filename") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + two_word_flags+=("-f") + flags_with_completion+=("-f") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + local_nonpersistent_flags+=("--filename=") + flags+=("--from-key=") + local_nonpersistent_flags+=("--from-key=") + flags+=("--include=") + local_nonpersistent_flags+=("--include=") + flags+=("--no-headers") + local_nonpersistent_flags+=("--no-headers") + flags+=("--output=") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output=") + flags+=("--show-all") + flags+=("-a") + local_nonpersistent_flags+=("--show-all") + flags+=("--show-labels") + local_nonpersistent_flags+=("--show-labels") + flags+=("--sort-by=") + local_nonpersistent_flags+=("--sort-by=") + flags+=("--template=") + flags_with_completion+=("--template") + flags_completion+=("_filedir") + local_nonpersistent_flags+=("--template=") + flags+=("--to-key=") + local_nonpersistent_flags+=("--to-key=") + flags+=("--as=") + flags+=("--as-group=") + flags+=("--cache-dir=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--loglevel=") + flags+=("--logspec=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--request-timeout=") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + flags+=("--v=") + flags+=("--version") + flags+=("--vmodule=") + + must_have_one_flag=() + must_have_one_flag+=("--filename=") + must_have_one_flag+=("-f") + must_have_one_noun=() + noun_aliases=() +} + _oc_adm_migrate_storage() { last_command="oc_adm_migrate_storage" @@ -4535,6 +4620,7 @@ _oc_adm_migrate() commands=() commands+=("etcd-ttl") commands+=("image-references") + commands+=("legacy-hpa") commands+=("storage") flags=() diff --git a/contrib/completions/zsh/oc b/contrib/completions/zsh/oc index 5e50fa24f82b..3836598ddfa3 100644 --- a/contrib/completions/zsh/oc +++ b/contrib/completions/zsh/oc @@ -4586,6 +4586,91 @@ _oc_adm_migrate_image-references() noun_aliases=() } +_oc_adm_migrate_legacy-hpa() +{ + last_command="oc_adm_migrate_legacy-hpa" + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--all-namespaces") + local_nonpersistent_flags+=("--all-namespaces") + flags+=("--allow-missing-template-keys") + local_nonpersistent_flags+=("--allow-missing-template-keys") + flags+=("--confirm") + local_nonpersistent_flags+=("--confirm") + flags+=("--filename=") + flags_with_completion+=("--filename") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + two_word_flags+=("-f") + flags_with_completion+=("-f") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + local_nonpersistent_flags+=("--filename=") + flags+=("--from-key=") + local_nonpersistent_flags+=("--from-key=") + flags+=("--include=") + local_nonpersistent_flags+=("--include=") + flags+=("--no-headers") + local_nonpersistent_flags+=("--no-headers") + flags+=("--output=") + two_word_flags+=("-o") + local_nonpersistent_flags+=("--output=") + flags+=("--show-all") + flags+=("-a") + local_nonpersistent_flags+=("--show-all") + flags+=("--show-labels") + local_nonpersistent_flags+=("--show-labels") + flags+=("--sort-by=") + local_nonpersistent_flags+=("--sort-by=") + flags+=("--template=") + flags_with_completion+=("--template") + flags_completion+=("_filedir") + local_nonpersistent_flags+=("--template=") + flags+=("--to-key=") + local_nonpersistent_flags+=("--to-key=") + flags+=("--as=") + flags+=("--as-group=") + flags+=("--cache-dir=") + flags+=("--certificate-authority=") + flags_with_completion+=("--certificate-authority") + flags_completion+=("_filedir") + flags+=("--client-certificate=") + flags_with_completion+=("--client-certificate") + flags_completion+=("_filedir") + flags+=("--client-key=") + flags_with_completion+=("--client-key") + flags_completion+=("_filedir") + flags+=("--cluster=") + flags+=("--config=") + flags_with_completion+=("--config") + flags_completion+=("_filedir") + flags+=("--context=") + flags+=("--insecure-skip-tls-verify") + flags+=("--log-flush-frequency=") + flags+=("--loglevel=") + flags+=("--logspec=") + flags+=("--match-server-version") + flags+=("--namespace=") + two_word_flags+=("-n") + flags+=("--request-timeout=") + flags+=("--server=") + flags+=("--token=") + flags+=("--user=") + flags+=("--v=") + flags+=("--version") + flags+=("--vmodule=") + + must_have_one_flag=() + must_have_one_flag+=("--filename=") + must_have_one_flag+=("-f") + must_have_one_noun=() + noun_aliases=() +} + _oc_adm_migrate_storage() { last_command="oc_adm_migrate_storage" @@ -4677,6 +4762,7 @@ _oc_adm_migrate() commands=() commands+=("etcd-ttl") commands+=("image-references") + commands+=("legacy-hpa") commands+=("storage") flags=() diff --git a/docs/man/man1/.files_generated_oc b/docs/man/man1/.files_generated_oc index a30aeb410038..9659ef4002e2 100644 --- a/docs/man/man1/.files_generated_oc +++ b/docs/man/man1/.files_generated_oc @@ -71,6 +71,7 @@ oc-adm-manage-node.1 oc-adm-migrate-authorization.1 oc-adm-migrate-etcd-ttl.1 oc-adm-migrate-image-references.1 +oc-adm-migrate-legacy-hpa.1 oc-adm-migrate-storage.1 oc-adm-migrate.1 oc-adm-new-project.1 diff --git a/docs/man/man1/oc-adm-migrate-legacy-hpa.1 b/docs/man/man1/oc-adm-migrate-legacy-hpa.1 new file mode 100644 index 000000000000..b6fd7a0f9896 --- /dev/null +++ b/docs/man/man1/oc-adm-migrate-legacy-hpa.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/pkg/oc/admin/admin.go b/pkg/oc/admin/admin.go index da2a30f3fd6f..9608cbe6ea83 100644 --- a/pkg/oc/admin/admin.go +++ b/pkg/oc/admin/admin.go @@ -21,6 +21,7 @@ import ( migrateauthorization "github.com/openshift/origin/pkg/oc/admin/migrate/authorization" migrateetcd "github.com/openshift/origin/pkg/oc/admin/migrate/etcd" migrateimages "github.com/openshift/origin/pkg/oc/admin/migrate/images" + migratehpa "github.com/openshift/origin/pkg/oc/admin/migrate/legacyhpa" migratestorage "github.com/openshift/origin/pkg/oc/admin/migrate/storage" "github.com/openshift/origin/pkg/oc/admin/network" "github.com/openshift/origin/pkg/oc/admin/node" @@ -97,6 +98,7 @@ func NewCommandAdmin(name, fullName string, in io.Reader, out io.Writer, errout migratestorage.NewCmdMigrateAPIStorage("storage", fullName+" "+migrate.MigrateRecommendedName+" storage", f, in, out, errout), migrateauthorization.NewCmdMigrateAuthorization("authorization", fullName+" "+migrate.MigrateRecommendedName+" authorization", f, in, out, errout), migrateetcd.NewCmdMigrateTTLs("etcd-ttl", fullName+" "+migrate.MigrateRecommendedName+" etcd-ttl", f, in, out, errout), + migratehpa.NewCmdMigrateLegacyHPA("legacy-hpa", fullName+" "+migrate.MigrateRecommendedName+" legacy-hpa", f, in, out, errout), ), top.NewCommandTop(top.TopRecommendedName, fullName+" "+top.TopRecommendedName, f, out, errout), image.NewCmdVerifyImageSignature(name, fullName+" "+image.VerifyRecommendedName, f, out, errout), diff --git a/pkg/oc/admin/migrate/legacyhpa/hpa.go b/pkg/oc/admin/migrate/legacyhpa/hpa.go new file mode 100644 index 000000000000..39189011c9d9 --- /dev/null +++ b/pkg/oc/admin/migrate/legacyhpa/hpa.go @@ -0,0 +1,190 @@ +package legacyhpa + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/spf13/cobra" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + autoscaling "k8s.io/kubernetes/pkg/apis/autoscaling" + autoscalingclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/autoscaling/internalversion" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + + "github.com/openshift/origin/pkg/oc/admin/migrate" + "github.com/openshift/origin/pkg/oc/cli/util/clientcmd" +) + +var ( + defaultMigrations = map[metav1.TypeMeta]metav1.TypeMeta{ + // legacy oapi group + {"DeploymentConfig", "v1"}: {"DeploymentConfig", "apps.openshift.io/v1"}, + // legacy oapi group, for the lazy + {"DeploymentConfig", ""}: {"DeploymentConfig", "apps.openshift.io/v1"}, + + // webconsole shenaniganry + {"DeploymentConfig", "extensions/v1beta1"}: {"DeploymentConfig", "apps.openshift.io/v1"}, + {"Deployment", "extensions/v1beta1"}: {"Deployment", "apps/v1"}, + {"ReplicaSet", "extensions/v1beta1"}: {"ReplicaSet", "apps/v1"}, + {"ReplicationController", "extensions/v1beta1"}: {"ReplicationController", "v1"}, + } + + internalMigrateLegacyHPALong = templates.LongDesc(fmt.Sprintf(` + Migrate Horizontal Pod Autoscalers to refer to new API groups + + This command locates and updates every Horizontal Pod Autoscaler which refers to a particular + group-version-kind to refer to some other, equivalent group-version-kind. + + The following transformations will occur: + +%s`, prettyPrintMigrations(defaultMigrations))) + + internalMigrateLegacyHPAExample = templates.Examples(` + # Perform a dry-run of updating all objects + %[1]s + + # To actually perform the update, the confirm flag must be appended + %[1]s --confirm + + # Migrate a specific group-version-kind to the latest preferred version + %[1]s --initial=extensions/v1beta1.ReplicaSet --confirm + + # Migrate a specific group-version-kind to a specific group-version-kind + %[1]s --initial=v1.DeploymentConfig --final=apps.openshift.io/v1.DeploymentConfig --confirm`) +) + +func prettyPrintMigrations(versionKinds map[metav1.TypeMeta]metav1.TypeMeta) string { + lines := make([]string, 0, len(versionKinds)) + for initial, final := range versionKinds { + line := fmt.Sprintf(" - %s.%s --> %s.%s", initial.APIVersion, initial.Kind, final.APIVersion, final.Kind) + lines = append(lines, line) + } + sort.Strings(lines) + + return strings.Join(lines, "\n") +} + +type MigrateLegacyHPAOptions struct { + // maps initial gvks to final gvks in the same format + // as HPAs use (CrossVersionObjectReferences) for ease of access. + finalVersionKinds map[metav1.TypeMeta]metav1.TypeMeta + + hpaClient autoscalingclient.AutoscalingInterface + + migrate.ResourceOptions +} + +// NewCmdMigrateLegacyAPI implements a MigrateLegacyHPA command +func NewCmdMigrateLegacyHPA(name, fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command { + options := &MigrateLegacyHPAOptions{ + ResourceOptions: migrate.ResourceOptions{ + In: in, + Out: out, + ErrOut: errout, + + AllNamespaces: true, + Include: []string{"horizontalpodautoscalers.autoscaling"}, + }, + } + cmd := &cobra.Command{ + Use: name, + Short: "Update HPAs to point to the latest group-version-kinds", + Long: internalMigrateLegacyHPALong, + Example: fmt.Sprintf(internalMigrateLegacyHPAExample, fullName), + Run: func(cmd *cobra.Command, args []string) { + kcmdutil.CheckErr(options.Complete(name, f, cmd, args)) + kcmdutil.CheckErr(options.Validate()) + kcmdutil.CheckErr(options.Run()) + }, + } + options.ResourceOptions.Bind(cmd) + + return cmd +} + +func (o *MigrateLegacyHPAOptions) Complete(name string, f *clientcmd.Factory, c *cobra.Command, args []string) error { + if len(args) != 0 { + return fmt.Errorf("%s takes no positional arguments", name) + } + + o.ResourceOptions.SaveFn = o.save + if err := o.ResourceOptions.Complete(f, c); err != nil { + return err + } + + o.finalVersionKinds = make(map[metav1.TypeMeta]metav1.TypeMeta) + + // copy all manual transformations in + for initial, final := range defaultMigrations { + o.finalVersionKinds[initial] = final + } + + kubeClientSet, err := f.ClientSet() + if err != nil { + return err + } + o.hpaClient = kubeClientSet.Autoscaling() + + return nil +} + +func (o MigrateLegacyHPAOptions) Validate() error { + if len(o.ResourceOptions.Include) != 1 || o.ResourceOptions.Include[0] != "horizontalpodautoscalers.autoscaling" { + return fmt.Errorf("the only supported resources are horizontalpodautoscalers") + } + return o.ResourceOptions.Validate() +} + +func (o MigrateLegacyHPAOptions) Run() error { + return o.ResourceOptions.Visitor().Visit(func(info *resource.Info) (migrate.Reporter, error) { + return o.checkAndTransform(info.Object) + }) +} + +func (o *MigrateLegacyHPAOptions) checkAndTransform(hpaRaw runtime.Object) (migrate.Reporter, error) { + hpa, wasHPA := hpaRaw.(*autoscaling.HorizontalPodAutoscaler) + if !wasHPA { + return nil, fmt.Errorf("unrecognized object %#v", hpaRaw) + } + + currentVersionKind := metav1.TypeMeta{ + APIVersion: hpa.Spec.ScaleTargetRef.APIVersion, + Kind: hpa.Spec.ScaleTargetRef.Kind, + } + + newVersionKind := o.latestVersionKind(currentVersionKind) + + if currentVersionKind != newVersionKind { + hpa.Spec.ScaleTargetRef.APIVersion = newVersionKind.APIVersion + hpa.Spec.ScaleTargetRef.Kind = newVersionKind.Kind + return migrate.ReporterBool(true), nil + } + + return migrate.ReporterBool(false), nil +} + +func (o *MigrateLegacyHPAOptions) latestVersionKind(current metav1.TypeMeta) metav1.TypeMeta { + if newVersionKind, isKnown := o.finalVersionKinds[current]; isKnown { + return newVersionKind + } + + return current +} + +// save invokes the API to alter an object. The reporter passed to this method is the same returned by +// the migration visitor method. It should return an error if the input type cannot be saved +// It returns migrate.ErrRecalculate if migration should be re-run on the provided object. +func (o *MigrateLegacyHPAOptions) save(info *resource.Info, reporter migrate.Reporter) error { + hpa, wasHPA := info.Object.(*autoscaling.HorizontalPodAutoscaler) + if !wasHPA { + return fmt.Errorf("unrecognized object %#v", info.Object) + } + + _, err := o.hpaClient.HorizontalPodAutoscalers(hpa.Namespace).Update(hpa) + return migrate.DefaultRetriable(info, err) +} diff --git a/pkg/oc/admin/migrate/legacyhpa/hpa_test.go b/pkg/oc/admin/migrate/legacyhpa/hpa_test.go new file mode 100644 index 000000000000..a6b93667df73 --- /dev/null +++ b/pkg/oc/admin/migrate/legacyhpa/hpa_test.go @@ -0,0 +1,93 @@ +package legacyhpa + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/apis/autoscaling" +) + +func TestDefaultMigrations(t *testing.T) { + testCases := []struct { + name string + input metav1.TypeMeta + output metav1.TypeMeta + }{ + { + name: "legacy-dc", + input: metav1.TypeMeta{"DeploymentConfig", "v1"}, + output: metav1.TypeMeta{"DeploymentConfig", "apps.openshift.io/v1"}, + }, + { + name: "console-dc", + input: metav1.TypeMeta{"DeploymentConfig", "extensions/v1beta1"}, + output: metav1.TypeMeta{"DeploymentConfig", "apps.openshift.io/v1"}, + }, + { + name: "console-rc", + input: metav1.TypeMeta{"ReplicationController", "extensions/v1beta1"}, + output: metav1.TypeMeta{"ReplicationController", "v1"}, + }, + { + name: "console-deploy", + input: metav1.TypeMeta{"Deployment", "extensions/v1beta1"}, + output: metav1.TypeMeta{"Deployment", "apps/v1"}, + }, + { + name: "console-rs", + input: metav1.TypeMeta{"ReplicaSet", "extensions/v1beta1"}, + output: metav1.TypeMeta{"ReplicaSet", "apps/v1"}, + }, + { + name: "ok-dc", + input: metav1.TypeMeta{"DeploymentConfig", "apps.openshift.io/v1"}, + output: metav1.TypeMeta{"DeploymentConfig", "apps.openshift.io/v1"}, + }, + { + name: "other", + input: metav1.TypeMeta{"Cheddar", "cheese/v1alpha1"}, + output: metav1.TypeMeta{"Cheddar", "cheese/v1alpha1"}, + }, + } + + opts := MigrateLegacyHPAOptions{ + finalVersionKinds: defaultMigrations, + } + + for _, tc := range testCases { + tc := tc // copy the iteration variable to a non-iteration memory location + t.Run(tc.name, func(t *testing.T) { + oldHPA := &autoscaling.HorizontalPodAutoscaler{ + Spec: autoscaling.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: autoscaling.CrossVersionObjectReference{ + APIVersion: tc.input.APIVersion, + Kind: tc.input.Kind, + Name: tc.name, + }, + }, + } + + reporter, err := opts.checkAndTransform(oldHPA) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedChanged := tc.input != tc.output + if reporter.Changed() != expectedChanged { + indicator := "" + if expectedChanged { + indicator = " not" + } + t.Errorf("expected the HPA%s to have been changed, but it had%s", indicator, indicator) + } + newVersionKind := metav1.TypeMeta{ + APIVersion: oldHPA.Spec.ScaleTargetRef.APIVersion, + Kind: oldHPA.Spec.ScaleTargetRef.Kind, + } + if newVersionKind != tc.output { + t.Errorf("expected the HPA to be updated to %v, yet it ended up as %v", tc.output, newVersionKind) + } + }) + + } +} diff --git a/test/cmd/migrate.sh b/test/cmd/migrate.sh index 7f5d00f355d6..0b5fa4c48c8b 100755 --- a/test/cmd/migrate.sh +++ b/test/cmd/migrate.sh @@ -78,4 +78,14 @@ os::cmd::expect_success_and_not_text 'oc get is test --template "{{ range .statu os::cmd::expect_success_and_not_text 'oc get is test --template "{{ range .status.tags }}{{ range .items }}{{ .dockerImageReference }}{{ \"\n\" }}{{ end }}{{ end }}"' '^mysql' os::test::junit::declare_suite_end +os::test::junit::declare_suite_start "cmd/migrate/legacyhpa" +# create a legacy and a normal HPA +os::cmd::expect_success 'oc create -f test/testdata/hpa/legacy-and-normal-hpa.yaml' +# verify dry run +os::cmd::expect_success_and_text 'oc adm migrate legacy-hpa' 'migrated=1' +# verify that all HPAs are as they should be +os::cmd::expect_success_and_text 'oc get hpa legacy-hpa -o jsonpath="{.spec.scaleTargetRef.apiVersion}.{.spec.scaleTargetRef.kind} {.spec.scaleTargetRef.name}"' 'apps.openshift.io/v1.DeploymentConfig legacy-target' +os::cmd::expect_success_and_text 'oc get hpa other-hpa -o jsonpath="{.spec.scaleTargetRef.apiVersion}.{.spec.scaleTargetRef.kind} {.spec.scaleTargetRef.name}"' 'apps/v1.Deployment other-target' +os::test::junit::declare_suite_end + os::test::junit::declare_suite_end diff --git a/test/testdata/hpa/legacy-and-normal-hpa.yaml b/test/testdata/hpa/legacy-and-normal-hpa.yaml new file mode 100644 index 000000000000..2c552b6acb82 --- /dev/null +++ b/test/testdata/hpa/legacy-and-normal-hpa.yaml @@ -0,0 +1,22 @@ +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: legacy-hpa +spec: + maxReplicas: 10 + minReplicas: 2 + scaleTargetRef: + kind: DeploymentConfig + name: legacy-target +--- +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: legacy-hpa +spec: + maxReplicas: 10 + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: other-target