diff --git a/cmpserver/plugin/plugin.go b/cmpserver/plugin/plugin.go index e8cfb01ecb3e9..5bc621783ddc9 100644 --- a/cmpserver/plugin/plugin.go +++ b/cmpserver/plugin/plugin.go @@ -332,9 +332,9 @@ func (s *Service) GetParametersAnnouncement(stream apiclient.ConfigManagementPlu } func getParametersAnnouncement(ctx context.Context, appDir string, staticAnnouncements []Static, command Command) (*apiclient.ParametersAnnouncementResponse, error) { - var staticParamAnnouncements []*apiclient.ParameterAnnouncement + var announcements []*apiclient.ParameterAnnouncement for _, static := range staticAnnouncements { - staticParamAnnouncements = append(staticParamAnnouncements, &apiclient.ParameterAnnouncement{ + announcements = append(announcements, &apiclient.ParameterAnnouncement{ Name: static.Name, Title: static.Title, Tooltip: static.Tooltip, @@ -347,19 +347,24 @@ func getParametersAnnouncement(ctx context.Context, appDir string, staticAnnounc }) } - stdout, err := runCommand(ctx, command, appDir, os.Environ()) - if err != nil { - return nil, fmt.Errorf("error executing dynamic parameter output command: %s", err) - } + if len(command.Command) > 0 { + stdout, err := runCommand(ctx, command, appDir, os.Environ()) + if err != nil { + return nil, fmt.Errorf("error executing dynamic parameter output command: %s", err) + } - var dynamicParamAnnouncements []*apiclient.ParameterAnnouncement - err = json.Unmarshal([]byte(stdout), &dynamicParamAnnouncements) - if err != nil { - return nil, fmt.Errorf("error unmarshaling dynamic parameter output into ParametersAnnouncementResponse: %s", err) + var dynamicParamAnnouncements []*apiclient.ParameterAnnouncement + err = json.Unmarshal([]byte(stdout), &dynamicParamAnnouncements) + if err != nil { + return nil, fmt.Errorf("error unmarshaling dynamic parameter output into ParametersAnnouncementResponse: %s", err) + } + + // dynamic goes first, because static should take precedence by being later. + announcements = append(dynamicParamAnnouncements, announcements...) } repoResponse := &apiclient.ParametersAnnouncementResponse{ - ParameterAnnouncements: append(staticParamAnnouncements, dynamicParamAnnouncements...), + ParameterAnnouncements: announcements, } return repoResponse, nil } diff --git a/cmpserver/plugin/plugin_test.go b/cmpserver/plugin/plugin_test.go index 16e40aa20cef7..23f23bef07ad2 100644 --- a/cmpserver/plugin/plugin_test.go +++ b/cmpserver/plugin/plugin_test.go @@ -246,6 +246,20 @@ func Test_getParametersAnnouncement_empty_command(t *testing.T) { assert.Equal(t, []*apiclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements) } +func Test_getParametersAnnouncement_no_command(t *testing.T) { + staticYAML := ` +- name: static-a +- name: static-b +` + static := &[]Static{} + err := yaml.Unmarshal([]byte(staticYAML), static) + require.NoError(t, err) + command := Command{} + res, err := getParametersAnnouncement(context.Background(), "", *static, command) + require.NoError(t, err) + assert.Equal(t, []*apiclient.ParameterAnnouncement{{Name: "static-a"}, {Name: "static-b"}}, res.ParameterAnnouncements) +} + func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) { staticYAML := ` - name: static-a @@ -261,10 +275,10 @@ func Test_getParametersAnnouncement_static_and_dynamic(t *testing.T) { res, err := getParametersAnnouncement(context.Background(), "", *static, command) require.NoError(t, err) expected := []*apiclient.ParameterAnnouncement{ - {Name: "static-a"}, - {Name: "static-b"}, {Name: "dynamic-a"}, {Name: "dynamic-b"}, + {Name: "static-a"}, + {Name: "static-b"}, } assert.Equal(t, expected, res.ParameterAnnouncements) } @@ -278,3 +292,13 @@ func Test_getParametersAnnouncement_invalid_json(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "unexpected end of JSON input") } + +func Test_getParametersAnnouncement_bad_command(t *testing.T) { + command := Command{ + Command: []string{"exit"}, + Args: []string{"1"}, + } + _, err := getParametersAnnouncement(context.Background(), "", []Static{}, command) + assert.Error(t, err) + assert.Contains(t, err.Error(), "error executing dynamic parameter output command") +} diff --git a/test/cmp/plugin.yaml b/test/cmp/plugin.yaml index 1184b715d284d..28c735290a4da 100644 --- a/test/cmp/plugin.yaml +++ b/test/cmp/plugin.yaml @@ -11,3 +11,17 @@ spec: glob: "**/kustomization.yaml" allowConcurrency: true lockRepo: false + parameters: + static: + - name: string-param + string: value + - name: array-param + collectionType: array + array: + - value1 + - value2 + - name: map-param + collectionType: map + map: + key: value + key2: value2 diff --git a/ui/src/app/applications/components/application-parameters/application-parameters.tsx b/ui/src/app/applications/components/application-parameters/application-parameters.tsx index 972783777f682..f1070c373db72 100644 --- a/ui/src/app/applications/components/application-parameters/application-parameters.tsx +++ b/ui/src/app/applications/components/application-parameters/application-parameters.tsx @@ -278,6 +278,36 @@ export const ApplicationParameters = (props: { view: app.spec.source.plugin && (app.spec.source.plugin.env || []).map(i => `${i.name}='${i.value}'`).join(' '), edit: (formApi: FormApi) => }); + if (props.details.plugin.parametersAnnouncement) { + for (const announcement of props.details.plugin.parametersAnnouncement) { + const liveParam = app.spec.source.plugin.parameters?.find(param => param.name === announcement.name); + if (announcement.collectionType === undefined || announcement.collectionType === '' || announcement.collectionType === 'string') { + attributes.push({ + title: announcement.title ?? announcement.name, + view: liveParam?.string || announcement.string, + edit: () => liveParam?.string || announcement.string + }); + } else if (announcement.collectionType === 'array') { + attributes.push({ + title: announcement.title ?? announcement.name, + view: (liveParam?.array || announcement.array || []).join(' '), + edit: () => (liveParam?.array || announcement.array || []).join(' ') + }); + } else if (announcement.collectionType === 'map') { + const entries = concatMaps(announcement.map, liveParam?.map).entries(); + attributes.push({ + title: announcement.title ?? announcement.name, + view: Array.from(entries) + .map(([key, value]) => `${key}='${value}'`) + .join(' '), + edit: () => + Array.from(entries) + .map(([key, value]) => `${key}='${value}'`) + .join(' ') + }); + } + } + } } else if (props.details.type === 'Directory') { const directory = app.spec.source.directory || ({} as ApplicationSourceDirectory); attributes.push({ @@ -344,3 +374,16 @@ export const ApplicationParameters = (props: { /> ); }; + +// concatMaps merges two maps. Later args take precedence where there's a key conflict. +function concatMaps(...maps: (Map | null)[]): Map { + const newMap = new Map(); + for (const map of maps) { + if (map) { + for (const entry of Object.entries(map)) { + newMap.set(entry[0], entry[1]); + } + } + } + return newMap; +} diff --git a/ui/src/app/shared/models.ts b/ui/src/app/shared/models.ts index fb704556ef749..1ac95fd60c13b 100644 --- a/ui/src/app/shared/models.ts +++ b/ui/src/app/shared/models.ts @@ -209,6 +209,14 @@ export interface EnvEntry { export interface ApplicationSourcePlugin { name: string; env: EnvEntry[]; + parameters?: Parameter[]; +} + +export interface Parameter { + name: string; + string?: string; + array?: string[]; + map?: Map; } export interface JsonnetVar { @@ -579,6 +587,19 @@ export interface KustomizeAppSpec { export interface PluginAppSpec { name: string; env: EnvEntry[]; + parametersAnnouncement?: ParameterAnnouncement[]; +} + +export interface ParameterAnnouncement { + name?: string; + title?: string; + tooltip?: string; + required?: boolean; + itemType?: string; + collectionType?: string; + string?: string; + array?: string[]; + map?: Map; } export interface ObjectReference {