feat: UI write support CMP (#11754) (#12137)

* #11602 fix : Object options menu truncated when selected in ApplicationListView.

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* CMP parameter changes

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* fix: pointers to param values

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

better?

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

fix silliness

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

terrible hacks

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

maybe better codegen

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

fix tests

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* same prefix issue fixed

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* fix for delete param name

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* lint changes

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* lint fix

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* lint fix

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* finalChanges

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* Delete application-resource-list.tsx

Not needed for this PR.

Signed-off-by: schakrad <58915923+schakrad@users.noreply.github.com>

* added file which was deleted as it was not the change needed for this feature.

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* refactored MapValuField and added fix for some edge cases

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* Update application-resource-list.tsx

Reverting the change as this is not related to this PR.

Signed-off-by: schakrad <58915923+schakrad@users.noreply.github.com>

* Reverting the change in application-resource-list

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* Showing application parameter values irrespective of parameter present or not in plugin.yaml

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* fix for lint errors

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>

* fix false source mismatch

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix equals

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix swagger doc

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* Tooltip description change.

Signed-off-by: schakrad <chakradari.sindhu@gmail.com>

* fixed lint

Signed-off-by: schakrad <chakradari.sindhu@gmail.com>

* CMP fix for empty array.

Signed-off-by: schakrad <chakradari.sindhu@gmail.com>

---------

Signed-off-by: schakradari <saisindhu_chakradari@intuit.com>
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Signed-off-by: schakrad <58915923+schakrad@users.noreply.github.com>
Signed-off-by: schakrad <chakradari.sindhu@gmail.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
schakrad
2023-03-21 14:39:10 -04:00
committed by GitHub
parent 95626eeb81
commit faa7331f9d
15 changed files with 2021 additions and 947 deletions

View File

@@ -1479,7 +1479,7 @@ func currentSourceEqualsSyncedSource(app *appv1.Application) bool {
if app.Spec.HasMultipleSources() {
return app.Spec.Sources.Equals(app.Status.Sync.ComparedTo.Sources)
}
return app.Spec.Source.Equals(app.Status.Sync.ComparedTo.Source)
return app.Spec.Source.Equals(&app.Status.Sync.ComparedTo.Source)
}
// needRefreshAppStatus answers if application status needs to be refreshed.

View File

@@ -112,7 +112,13 @@ EOF
rm -f "${SWAGGER_OUT}"
find "${SWAGGER_ROOT}" -name '*.swagger.json' -exec swagger mixin --ignore-conflicts "${PRIMARY_SWAGGER}" '{}' \+ > "${COMBINED_SWAGGER}"
jq -r 'del(.definitions[].properties[]? | select(."$ref"!=null and .description!=null).description) | del(.definitions[].properties[]? | select(."$ref"!=null and .title!=null).title)' "${COMBINED_SWAGGER}" > "${SWAGGER_OUT}"
jq -r 'del(.definitions[].properties[]? | select(."$ref"!=null and .description!=null).description) | del(.definitions[].properties[]? | select(."$ref"!=null and .title!=null).title) |
# The "array" and "map" fields have custom unmarshaling. Modify the swagger to reflect this.
.definitions.v1alpha1ApplicationSourcePluginParameter.properties.array = {"description":"Array is the value of an array type parameter.","type":"array","items":{"type":"string"}} |
del(.definitions.v1alpha1OptionalArray) |
.definitions.v1alpha1ApplicationSourcePluginParameter.properties.map = {"description":"Map is the value of a map type parameter.","type":"object","additionalProperties":{"type":"string"}} |
del(.definitions.v1alpha1OptionalMap)
' "${COMBINED_SWAGGER}" > "${SWAGGER_OUT}"
/bin/rm "${PRIMARY_SWAGGER}" "${COMBINED_SWAGGER}"
}
@@ -128,3 +134,4 @@ clean_swagger server
clean_swagger reposerver
clean_swagger controller
clean_swagger cmpserver

View File

@@ -21,7 +21,6 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/ap
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSourceJsonnet,ExtVars
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSourceJsonnet,Libs
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSourceJsonnet,TLAs
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSourcePluginParameter,Array
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSpec,IgnoreDifferences
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationSpec,Info
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ApplicationStatus,Conditions
@@ -48,6 +47,7 @@ API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/ap
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,MergeGenerator,MergeKeys
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,NestedMergeGenerator,MergeKeys
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,Operation,Info
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,OptionalArray,Array
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,OrphanedResourcesMonitorSettings,Ignore
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,OverrideIgnoreDiff,JQPathExpressions
API rule violation: list_type_missing,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,OverrideIgnoreDiff,JSONPointers

File diff suppressed because it is too large Load Diff

View File

@@ -490,10 +490,10 @@ message ApplicationSourcePluginParameter {
optional string string = 5;
// Map is the value of a map type parameter.
map<string, string> map = 3;
optional OptionalMap map = 3;
// Array is the value of an array type parameter.
repeated string array = 4;
optional OptionalArray array = 4;
}
// ApplicationSpec represents desired application state. Contains link to repository with application definition and additional parameters link definition revision.
@@ -1117,6 +1117,18 @@ message OperationState {
optional int64 retryCount = 8;
}
message OptionalArray {
// Array is the value of an array type parameter.
// +optional
repeated string array = 1;
}
message OptionalMap {
// Map is the value of a map type parameter.
// +optional
map<string, string> map = 1;
}
// OrphanedResourceKey is a reference to a resource to be ignored from
message OrphanedResourceKey {
optional string group = 1;

View File

@@ -95,6 +95,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.Operation": schema_pkg_apis_application_v1alpha1_Operation(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OperationInitiator": schema_pkg_apis_application_v1alpha1_OperationInitiator(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OperationState": schema_pkg_apis_application_v1alpha1_OperationState(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OptionalArray": schema_pkg_apis_application_v1alpha1_OptionalArray(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OptionalMap": schema_pkg_apis_application_v1alpha1_OptionalMap(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OrphanedResourceKey": schema_pkg_apis_application_v1alpha1_OrphanedResourceKey(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings": schema_pkg_apis_application_v1alpha1_OrphanedResourcesMonitorSettings(ref),
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OverrideIgnoreDiff": schema_pkg_apis_application_v1alpha1_OverrideIgnoreDiff(ref),
@@ -1840,37 +1842,6 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSourcePluginParameter(ref c
Format: "",
},
},
"map": {
SchemaProps: spec.SchemaProps{
Description: "Map is the value of a map type parameter.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"array": {
SchemaProps: spec.SchemaProps{
Description: "Array is the value of an array type parameter.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
@@ -3986,6 +3957,61 @@ func schema_pkg_apis_application_v1alpha1_OperationState(ref common.ReferenceCal
}
}
func schema_pkg_apis_application_v1alpha1_OptionalArray(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"array": {
SchemaProps: spec.SchemaProps{
Description: "Array is the value of an array type parameter.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
}
}
func schema_pkg_apis_application_v1alpha1_OptionalMap(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"map": {
SchemaProps: spec.SchemaProps{
Description: "Map is the value of a map type parameter.",
Type: []string{"object"},
AdditionalProperties: &spec.SchemaOrBool{
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
},
},
}
}
func schema_pkg_apis_application_v1alpha1_OrphanedResourceKey(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{

View File

@@ -188,7 +188,7 @@ func (s ApplicationSources) Equals(other ApplicationSources) bool {
return false
}
for i := range s {
if !s[i].Equals(other[i]) {
if !s[i].Equals(&other[i]) {
return false
}
}
@@ -529,19 +529,155 @@ func (d *ApplicationSourceDirectory) IsZero() bool {
return d == nil || !d.Recurse && d.Jsonnet.IsZero()
}
type OptionalMap struct {
// Map is the value of a map type parameter.
// +optional
Map map[string]string `json:"map" protobuf:"bytes,1,rep,name=map"`
// We need the explicit +optional so that kube-builder generates the CRD without marking this as required.
}
// Equals returns true if the two OptionalMap objects are equal. We can't use reflect.DeepEqual because it will return
// false if one of the maps is nil and the other is an empty map. This is because the JSON unmarshaller will set the
// map to nil if it is empty, but the protobuf unmarshaller will set it to an empty map.
func (o *OptionalMap) Equals(other *OptionalMap) bool {
if o == nil && other == nil {
return true
}
if o == nil || other == nil {
return false
}
if len(o.Map) != len(other.Map) {
return false
}
if o.Map == nil && other.Map == nil {
return true
}
// The next two blocks are critical. Depending on whether the struct was populated from JSON or protobufs, the map
// field will be either nil or an empty map. They mean the same thing: the map is empty.
if o.Map == nil && len(other.Map) == 0 {
return true
}
if other.Map == nil && len(o.Map) == 0 {
return true
}
return reflect.DeepEqual(o.Map, other.Map)
}
type OptionalArray struct {
// Array is the value of an array type parameter.
// +optional
Array []string `json:"array" protobuf:"bytes,1,rep,name=array"`
// We need the explicit +optional so that kube-builder generates the CRD without marking this as required.
}
// Equals returns true if the two OptionalArray objects are equal. We can't use reflect.DeepEqual because it will return
// false if one of the arrays is nil and the other is an empty array. This is because the JSON unmarshaller will set the
// array to nil if it is empty, but the protobuf unmarshaller will set it to an empty array.
func (o *OptionalArray) Equals(other *OptionalArray) bool {
if o == nil && other == nil {
return true
}
if o == nil || other == nil {
return false
}
if len(o.Array) != len(other.Array) {
return false
}
if o.Array == nil && other.Array == nil {
return true
}
// The next two blocks are critical. Depending on whether the struct was populated from JSON or protobufs, the array
// field will be either nil or an empty array. They mean the same thing: the array is empty.
if o.Array == nil && len(other.Array) == 0 {
return true
}
if other.Array == nil && len(o.Array) == 0 {
return true
}
return reflect.DeepEqual(o.Array, other.Array)
}
type ApplicationSourcePluginParameter struct {
// We use pointers to structs because go-to-protobuf represents pointers to arrays/maps as repeated fields.
// These repeated fields have no way to represent "present but empty." So we would have no way to distinguish
// {name: parameters, array: []} from {name: parameter}
// By wrapping the array/map in a struct, we can use a pointer to the struct to represent "present but empty."
// Name is the name identifying a parameter.
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`
// String_ is the value of a string type parameter.
String_ *string `json:"string,omitempty" protobuf:"bytes,5,opt,name=string"`
// Map is the value of a map type parameter.
Map map[string]string `json:"map,omitempty" protobuf:"bytes,3,rep,name=map"`
*OptionalMap `json:",omitempty" protobuf:"bytes,3,rep,name=map"`
// Array is the value of an array type parameter.
Array []string `json:"array,omitempty" protobuf:"bytes,4,rep,name=array"`
*OptionalArray `json:",omitempty" protobuf:"bytes,4,rep,name=array"`
}
func (p ApplicationSourcePluginParameter) Equals(other ApplicationSourcePluginParameter) bool {
if p.Name != other.Name {
return false
}
if !reflect.DeepEqual(p.String_, other.String_) {
return false
}
return p.OptionalMap.Equals(other.OptionalMap) && p.OptionalArray.Equals(other.OptionalArray)
}
// MarshalJSON is a custom JSON marshaller for ApplicationSourcePluginParameter. We need this custom marshaler because,
// when ApplicationSourcePluginParameter is unmarshaled, either from JSON or protobufs, the fields inside OptionalMap and
// OptionalArray are not set. The default JSON marshaler marshals these as "null." But really what we want to represent
// is an empty map or array.
//
// There are efforts to change things upstream, but nothing has been merged yet. See https://github.com/golang/go/issues/37711
func (p ApplicationSourcePluginParameter) MarshalJSON() ([]byte, error) {
out := map[string]interface{}{}
out["name"] = p.Name
if p.String_ != nil {
out["string"] = p.String_
}
if p.OptionalMap != nil {
if p.OptionalMap.Map == nil {
// Nil is not the same as a nil map. Nil means the field was not set, while a nil map means the field was set to an empty map.
// Either way, we want to marshal it as "{}".
out["map"] = map[string]string{}
} else {
out["map"] = p.OptionalMap.Map
}
}
if p.OptionalArray != nil {
if p.OptionalArray.Array == nil {
// Nil is not the same as a nil array. Nil means the field was not set, while a nil array means the field was set to an empty array.
// Either way, we want to marshal it as "[]".
out["array"] = []string{}
} else {
out["array"] = p.OptionalArray.Array
}
}
bytes, err := json.Marshal(out)
if err != nil {
return nil, err
}
return bytes, nil
}
type ApplicationSourcePluginParameters []ApplicationSourcePluginParameter
func (p ApplicationSourcePluginParameters) Equals(other ApplicationSourcePluginParameters) bool {
if len(p) != len(other) {
return false
}
for i := range p {
if !p[i].Equals(other[i]) {
return false
}
}
return true
}
func (p ApplicationSourcePluginParameters) IsZero() bool {
return len(p) == 0
}
// Environ builds a list of environment variables to represent parameters sent to a plugin from the Application
// manifest. Parameters are represented as one large stringified JSON array (under `ARGOCD_APP_PARAMETERS`). They're
// also represented as individual environment variables, each variable's key being an escaped version of the parameter's
@@ -560,13 +696,13 @@ func (p ApplicationSourcePluginParameters) Environ() ([]string, error) {
if param.String_ != nil {
env = append(env, fmt.Sprintf("%s=%s", envBaseName, *param.String_))
}
if param.Map != nil {
for key, value := range param.Map {
if param.OptionalMap != nil {
for key, value := range param.OptionalMap.Map {
env = append(env, fmt.Sprintf("%s_%s=%s", envBaseName, escaped(key), value))
}
}
if param.Array != nil {
for i, value := range param.Array {
if param.OptionalArray != nil {
for i, value := range param.OptionalArray.Array {
env = append(env, fmt.Sprintf("%s_%d=%s", envBaseName, i, value))
}
}
@@ -588,9 +724,28 @@ type ApplicationSourcePlugin struct {
Parameters ApplicationSourcePluginParameters `json:"parameters,omitempty" protobuf:"bytes,3,opt,name=parameters"`
}
func (c *ApplicationSourcePlugin) Equals(other *ApplicationSourcePlugin) bool {
if c == nil && other == nil {
return true
}
if c == nil || other == nil {
return false
}
if !c.Parameters.Equals(other.Parameters) {
return false
}
// DeepEqual works fine for fields besides Parameters. Since we already know that Parameters are equal, we can
// set them to nil and then do a DeepEqual.
leftCopy := c.DeepCopy()
rightCopy := other.DeepCopy()
leftCopy.Parameters = nil
rightCopy.Parameters = nil
return reflect.DeepEqual(leftCopy, rightCopy)
}
// IsZero returns true if the ApplicationSourcePlugin is considered empty
func (c *ApplicationSourcePlugin) IsZero() bool {
return c == nil || c.Name == "" && c.Env.IsZero()
return c == nil || c.Name == "" && c.Env.IsZero() && c.Parameters.IsZero()
}
// AddEnvEntry merges an EnvEntry into a list of entries. If an entry with the same name already exists,
@@ -2417,8 +2572,23 @@ func (condition *ApplicationCondition) IsError() bool {
}
// Equals compares two instances of ApplicationSource and return true if instances are equal.
func (source *ApplicationSource) Equals(other ApplicationSource) bool {
return reflect.DeepEqual(*source, other)
func (source *ApplicationSource) Equals(other *ApplicationSource) bool {
if source == nil && other == nil {
return true
}
if source == nil || other == nil {
return false
}
if !source.Plugin.Equals(other.Plugin) {
return false
}
// reflect.DeepEqual works fine for the other fields. Since the plugin fields are equal, set them to null so they're
// not considered in the DeepEqual comparison.
sourceCopy := source.DeepCopy()
otherCopy := other.DeepCopy()
sourceCopy.Plugin = nil
otherCopy.Plugin = nil
return reflect.DeepEqual(sourceCopy, otherCopy)
}
// ExplicitType returns the type (e.g. Helm, Kustomize, etc) of the application. If either none or multiple types are defined, returns an error.

View File

@@ -3,7 +3,7 @@ package v1alpha1
import (
"encoding/json"
"errors"
fmt "fmt"
"fmt"
"os"
"path"
"reflect"
@@ -11,10 +11,11 @@ import (
"testing"
"time"
argocdcommon "github.com/argoproj/argo-cd/v2/common"
"github.com/stretchr/testify/require"
"k8s.io/utils/pointer"
argocdcommon "github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -863,9 +864,9 @@ func TestAppSourceEquality(t *testing.T) {
},
}
right := left.DeepCopy()
assert.True(t, left.Equals(*right))
assert.True(t, left.Equals(right))
right.Directory.Recurse = false
assert.False(t, left.Equals(*right))
assert.False(t, left.Equals(right))
}
func TestAppDestinationEquality(t *testing.T) {
@@ -3261,8 +3262,8 @@ func TestApplicationSourcePluginParameters_Environ_string(t *testing.T) {
func TestApplicationSourcePluginParameters_Environ_array(t *testing.T) {
params := ApplicationSourcePluginParameters{
{
Name: "dependencies",
Array: []string{"redis", "minio"},
Name: "dependencies",
OptionalArray: &OptionalArray{Array: []string{"redis", "minio"}},
},
}
environ, err := params.Environ()
@@ -3279,9 +3280,11 @@ func TestApplicationSourcePluginParameters_Environ_map(t *testing.T) {
params := ApplicationSourcePluginParameters{
{
Name: "helm-parameters",
Map: map[string]string{
"image.repo": "quay.io/argoproj/argo-cd",
"image.tag": "v2.4.0",
OptionalMap: &OptionalMap{
Map: map[string]string{
"image.repo": "quay.io/argoproj/argo-cd",
"image.tag": "v2.4.0",
},
},
},
}
@@ -3302,10 +3305,14 @@ func TestApplicationSourcePluginParameters_Environ_all(t *testing.T) {
{
Name: "some-name",
String_: pointer.String("1.2.3"),
Array: []string{"redis", "minio"},
Map: map[string]string{
"image.repo": "quay.io/argoproj/argo-cd",
"image.tag": "v2.4.0",
OptionalArray: &OptionalArray{
Array: []string{"redis", "minio"},
},
OptionalMap: &OptionalMap{
Map: map[string]string{
"image.repo": "quay.io/argoproj/argo-cd",
"image.tag": "v2.4.0",
},
},
},
}
@@ -3401,3 +3408,89 @@ func TestGetSources(t *testing.T) {
})
}
}
func TestOptionalArrayEquality(t *testing.T) {
// Demonstrate that the JSON unmarshalling of an empty array parameter is an OptionalArray with the array field set
// to an empty array.
presentButEmpty := `{"array":[]}`
param := ApplicationSourcePluginParameter{}
err := json.Unmarshal([]byte(presentButEmpty), &param)
require.NoError(t, err)
jsonPresentButEmpty := param.OptionalArray
require.Equal(t, &OptionalArray{Array: []string{}}, jsonPresentButEmpty)
// We won't simulate the protobuf unmarshalling of an empty array parameter. By experimentation, this is how it's
// unmarshalled.
protobufPresentButEmpty := &OptionalArray{Array: nil}
tests := []struct {
name string
a *OptionalArray
b *OptionalArray
expected bool
}{
{"nil and nil", nil, nil, true},
{"nil and empty", nil, jsonPresentButEmpty, false},
{"nil and empty-containing-nil", nil, protobufPresentButEmpty, false},
{"empty-containing-empty and nil", jsonPresentButEmpty, nil, false},
{"empty-containing-nil and nil", protobufPresentButEmpty, nil, false},
{"empty-containing-empty and empty-containing-empty", jsonPresentButEmpty, jsonPresentButEmpty, true},
{"empty-containing-empty and empty-containing-nil", jsonPresentButEmpty, protobufPresentButEmpty, true},
{"empty-containing-nil and empty-containing-empty", protobufPresentButEmpty, jsonPresentButEmpty, true},
{"empty-containing-nil and empty-containing-nil", protobufPresentButEmpty, protobufPresentButEmpty, true},
{"empty-containing-empty and non-empty", jsonPresentButEmpty, &OptionalArray{Array: []string{"a"}}, false},
{"non-empty and empty-containing-nil", &OptionalArray{Array: []string{"a"}}, jsonPresentButEmpty, false},
{"non-empty and non-empty", &OptionalArray{Array: []string{"a"}}, &OptionalArray{Array: []string{"a"}}, true},
{"non-empty and non-empty different", &OptionalArray{Array: []string{"a"}}, &OptionalArray{Array: []string{"b"}}, false},
}
for _, testCase := range tests {
testCopy := testCase
t.Run(testCopy.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, testCopy.expected, testCopy.a.Equals(testCopy.b))
})
}
}
func TestOptionalMapEquality(t *testing.T) {
// Demonstrate that the JSON unmarshalling of an empty map parameter is an OptionalMap with the map field set
// to an empty map.
presentButEmpty := `{"map":{}}`
param := ApplicationSourcePluginParameter{}
err := json.Unmarshal([]byte(presentButEmpty), &param)
require.NoError(t, err)
jsonPresentButEmpty := param.OptionalMap
require.Equal(t, &OptionalMap{Map: map[string]string{}}, jsonPresentButEmpty)
// We won't simulate the protobuf unmarshalling of an empty map parameter. By experimentation, this is how it's
// unmarshalled.
protobufPresentButEmpty := &OptionalMap{Map: nil}
tests := []struct {
name string
a *OptionalMap
b *OptionalMap
expected bool
}{
{"nil and nil", nil, nil, true},
{"nil and empty-containing-empty", nil, jsonPresentButEmpty, false},
{"nil and empty-containing-nil", nil, protobufPresentButEmpty, false},
{"empty-containing-empty and nil", jsonPresentButEmpty, nil, false},
{"empty-containing-nil and nil", protobufPresentButEmpty, nil, false},
{"empty-containing-empty and empty-containing-empty", jsonPresentButEmpty, jsonPresentButEmpty, true},
{"empty-containing-empty and empty-containing-nil", jsonPresentButEmpty, protobufPresentButEmpty, true},
{"empty-containing-empty and non-empty", jsonPresentButEmpty, &OptionalMap{Map: map[string]string{"a": "b"}}, false},
{"empty-containing-nil and empty-containing-empty", protobufPresentButEmpty, jsonPresentButEmpty, true},
{"empty-containing-nil and empty-containing-nil", protobufPresentButEmpty, protobufPresentButEmpty, true},
{"non-empty and empty-containing-empty", &OptionalMap{Map: map[string]string{"a": "b"}}, jsonPresentButEmpty, false},
{"non-empty and non-empty", &OptionalMap{Map: map[string]string{"a": "b"}}, &OptionalMap{Map: map[string]string{"a": "b"}}, true},
{"non-empty and non-empty different", &OptionalMap{Map: map[string]string{"a": "b"}}, &OptionalMap{Map: map[string]string{"a": "c"}}, false},
}
for _, testCase := range tests {
testCopy := testCase
t.Run(testCopy.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, testCopy.expected, testCopy.a.Equals(testCopy.b))
})
}
}

View File

@@ -1051,17 +1051,15 @@ func (in *ApplicationSourcePluginParameter) DeepCopyInto(out *ApplicationSourceP
*out = new(string)
**out = **in
}
if in.Map != nil {
in, out := &in.Map, &out.Map
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
if in.OptionalMap != nil {
in, out := &in.OptionalMap, &out.OptionalMap
*out = new(OptionalMap)
(*in).DeepCopyInto(*out)
}
if in.Array != nil {
in, out := &in.Array, &out.Array
*out = make([]string, len(*in))
copy(*out, *in)
if in.OptionalArray != nil {
in, out := &in.OptionalArray, &out.OptionalArray
*out = new(OptionalArray)
(*in).DeepCopyInto(*out)
}
return
}
@@ -2307,6 +2305,50 @@ func (in *OperationState) DeepCopy() *OperationState {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OptionalArray) DeepCopyInto(out *OptionalArray) {
*out = *in
if in.Array != nil {
in, out := &in.Array, &out.Array
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionalArray.
func (in *OptionalArray) DeepCopy() *OptionalArray {
if in == nil {
return nil
}
out := new(OptionalArray)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OptionalMap) DeepCopyInto(out *OptionalMap) {
*out = *in
if in.Map != nil {
in, out := &in.Map, &out.Map
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OptionalMap.
func (in *OptionalMap) DeepCopy() *OptionalMap {
if in == nil {
return nil
}
out := new(OptionalMap)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *OrphanedResourceKey) DeepCopyInto(out *OrphanedResourceKey) {
*out = *in

View File

@@ -1,11 +1,10 @@
package repository
import (
"context"
"fmt"
"reflect"
"context"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/text"
log "github.com/sirupsen/logrus"
@@ -559,7 +558,8 @@ func (s *Server) isRepoPermittedInProject(ctx context.Context, repo string, proj
// isSourceInHistory checks if the supplied application source is either our current application
// source, or was something which we synced to previously.
func isSourceInHistory(app *v1alpha1.Application, source v1alpha1.ApplicationSource) bool {
if source.Equals(app.Spec.GetSource()) {
appSource := app.Spec.GetSource()
if source.Equals(&appSource) {
return true
}
// Iterate history. When comparing items in our history, use the actual synced revision to
@@ -568,7 +568,7 @@ func isSourceInHistory(app *v1alpha1.Application, source v1alpha1.ApplicationSou
// history[].revision will contain the explicit SHA
for _, h := range app.Status.History {
h.Source.TargetRevision = h.Revision
if source.Equals(h.Source) {
if source.Equals(&h.Source) {
return true
}
}

View File

@@ -4,6 +4,8 @@ metadata:
name: cmp-plugin
spec:
version: v1.0
init:
command: [env]
generate:
command: [sh, -c, 'kustomize build']
discover:

View File

@@ -1,8 +1,21 @@
import {AutocompleteField, DataLoader, FormField, FormSelect, getNestedField} from 'argo-ui';
import * as React from 'react';
import {FieldApi, FormApi, FormField as ReactFormField, Text, TextArea} from 'react-form';
import {ArrayInputField, CheckboxField, EditablePanel, EditablePanelItem, Expandable, TagsInputField} from '../../../shared/components';
import {cloneDeep} from 'lodash-es';
import {
ArrayInputField,
ArrayValueField,
CheckboxField,
EditablePanel,
EditablePanelItem,
Expandable,
MapValueField,
NameValueEditor,
StringValueField,
NameValue,
TagsInputField,
ValueEditor
} from '../../../shared/components';
import * as models from '../../../shared/models';
import {ApplicationSourceDirectory, Plugin} from '../../../shared/models';
import {services} from '../../../shared/services';
@@ -117,6 +130,7 @@ export const ApplicationParameters = (props: {
const [removedOverrides, setRemovedOverrides] = React.useState(new Array<boolean>());
let attributes: EditablePanelItem[] = [];
const [appParamsDeletedState, setAppParamsDeletedState] = React.useState([]);
if (props.details.type === 'Kustomize' && props.details.kustomize) {
attributes.push({
@@ -262,7 +276,7 @@ export const ApplicationParameters = (props: {
} else if (props.details.type === 'Plugin') {
attributes.push({
title: 'NAME',
view: source.plugin && source.plugin.name,
view: <div style={{marginTop: 15, marginBottom: 5}}>{ValueEditor(app.spec.source.plugin && app.spec.source.plugin.name, null)}</div>,
edit: (formApi: FormApi) => (
<DataLoader load={() => services.authService.plugins()}>
{(plugins: Plugin[]) => (
@@ -273,39 +287,160 @@ export const ApplicationParameters = (props: {
});
attributes.push({
title: 'ENV',
view: source.plugin && (source.plugin.env || []).map(i => `${i.name}='${i.value}'`).join(' '),
view: (
<div style={{marginTop: 15}}>
{app.spec.source.plugin &&
(app.spec.source.plugin.env || []).map(val => (
<span key={val.name} style={{display: 'block', marginBottom: 5}}>
{NameValueEditor(val, null)}
</span>
))}
</div>
),
edit: (formApi: FormApi) => <FormField field='spec.source.plugin.env' formApi={formApi} component={ArrayInputField} />
});
if (props.details.plugin.parametersAnnouncement) {
const parametersSet = new Set<string>();
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(' ')
});
}
parametersSet.add(announcement.name);
}
}
if (app.spec.source.plugin?.parameters) {
for (const appParameter of app.spec.source.plugin.parameters) {
parametersSet.add(appParameter.name);
}
}
for (const key of appParamsDeletedState) {
parametersSet.delete(key);
}
parametersSet.forEach(name => {
const announcement = props.details.plugin.parametersAnnouncement?.find(param => param.name === name);
const liveParam = app.spec.source.plugin?.parameters?.find(param => param.name === name);
const pluginIcon =
announcement && liveParam ? 'This parameter has been provided by plugin, but is overridden in application manifest.' : 'This parameter is provided by the plugin.';
const isPluginPar = announcement ? true : false;
if ((announcement?.collectionType === undefined && liveParam?.map) || announcement?.collectionType === 'map') {
let liveParamMap;
if (liveParam) {
liveParamMap = liveParam.map ?? new Map<string, string>();
}
const map = concatMaps(liveParamMap ?? announcement?.map, new Map<string, string>());
const entries = map.entries();
const items = new Array<NameValue>();
Array.from(entries).forEach(([key, value]) => items.push({name: key, value: `${value}`}));
attributes.push({
title: announcement?.title ?? announcement?.name ?? name,
customTitle: (
<span>
{isPluginPar && <i className='fa solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
{announcement?.title ?? announcement?.name ?? name}
</span>
),
view: (
<div style={{marginTop: 15, marginBottom: 5}}>
{items.length === 0 && <span style={{color: 'dimgray'}}>-- NO ITEMS --</span>}
{items.map(val => (
<span key={val.name} style={{display: 'block', marginBottom: 5}}>
{NameValueEditor(val)}
</span>
))}
</div>
),
edit: (formApi: FormApi) => (
<FormField
field='spec.source.plugin.parameters'
componentProps={{
name: announcement?.title ?? announcement?.name ?? name,
defaultVal: announcement?.map,
isPluginPar,
setAppParamsDeletedState
}}
formApi={formApi}
component={MapValueField}
/>
)
});
} else if ((announcement?.collectionType === undefined && liveParam?.array) || announcement?.collectionType === 'array') {
let liveParamArray;
if (liveParam) {
liveParamArray = liveParam?.array ?? [];
}
attributes.push({
title: announcement?.title ?? announcement?.name ?? name,
customTitle: (
<span>
{isPluginPar && <i className='fa-solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
{announcement?.title ?? announcement?.name ?? name}
</span>
),
view: (
<div style={{marginTop: 15, marginBottom: 5}}>
{(liveParamArray ?? announcement?.array ?? []).length === 0 && <span style={{color: 'dimgray'}}>-- NO ITEMS --</span>}
{(liveParamArray ?? announcement?.array ?? []).map((val, index) => (
<span key={index} style={{display: 'block', marginBottom: 5}}>
{ValueEditor(val, null)}
</span>
))}
</div>
),
edit: (formApi: FormApi) => (
<FormField
field='spec.source.plugin.parameters'
componentProps={{
name: announcement?.title ?? announcement?.name ?? name,
defaultVal: announcement?.array,
isPluginPar,
setAppParamsDeletedState
}}
formApi={formApi}
component={ArrayValueField}
/>
)
});
} else if (
(announcement?.collectionType === undefined && liveParam?.string) ||
announcement?.collectionType === '' ||
announcement?.collectionType === 'string' ||
announcement?.collectionType === undefined
) {
let liveParamString;
if (liveParam) {
liveParamString = liveParam?.string ?? '';
}
attributes.push({
title: announcement?.title ?? announcement?.name ?? name,
customTitle: (
<span>
{isPluginPar && <i className='fa-solid fa-puzzle-piece' title={pluginIcon} style={{marginRight: 5}} />}
{announcement?.title ?? announcement?.name ?? name}
</span>
),
view: (
<div
style={{
marginTop: 15,
marginBottom: 5
}}>
{ValueEditor(liveParamString ?? announcement?.string, null)}
</div>
),
edit: (formApi: FormApi) => (
<FormField
field='spec.source.plugin.parameters'
componentProps={{
name: announcement?.title ?? announcement?.name ?? name,
defaultVal: announcement?.string,
isPluginPar,
setAppParamsDeletedState
}}
formApi={formApi}
component={StringValueField}
/>
)
});
}
});
} else if (props.details.type === 'Directory') {
const directory = source.directory || ({} as ApplicationSourceDirectory);
attributes.push({
@@ -351,6 +486,7 @@ export const ApplicationParameters = (props: {
props.save &&
(async (input: models.Application) => {
const src = getAppDefaultSource(input);
function isDefined(item: any) {
return item !== null && item !== undefined;
}
@@ -364,11 +500,30 @@ export const ApplicationParameters = (props: {
if (src.kustomize && src.kustomize.images) {
src.kustomize.images = src.kustomize.images.filter(isDefinedWithVersion);
}
let params = input.spec?.source?.plugin?.parameters;
if (params) {
for (const param of params) {
if (param.map && param.array) {
// @ts-ignore
param.map = param.array.reduce((acc, {name, value}) => {
// @ts-ignore
acc[name] = value;
return acc;
}, {});
delete param.array;
}
}
params = params.filter(param => !appParamsDeletedState.includes(param.name));
input.spec.source.plugin.parameters = params;
}
await props.save(input, {});
setRemovedOverrides(new Array<boolean>());
})
}
values={app}
values={((props.details.plugin || app?.spec?.source?.plugin) && cloneDeep(app)) || app}
validate={updatedApp => {
const errors = {} as any;
@@ -379,6 +534,12 @@ export const ApplicationParameters = (props: {
return errors;
}}
onModeSwitch={
props.details.plugin &&
(() => {
setAppParamsDeletedState([]);
})
}
title={props.details.type.toLocaleUpperCase()}
items={attributes}
noReadonlyMode={props.noReadonlyMode}

View File

@@ -1,5 +1,6 @@
import * as React from 'react';
import * as ReactForm from 'react-form';
import {FormValue} from 'react-form';
/*
This provide a way to may a form field to an array of items. It allows you to
@@ -26,32 +27,53 @@ export interface NameValue {
value: string;
}
export const NameValueEditor = (item: NameValue, onChange: (item: NameValue) => any) => (
<React.Fragment>
export const NameValueEditor = (item: NameValue, onChange?: (item: NameValue) => any) => {
return (
<React.Fragment>
<input
// disable chrome autocomplete
autoComplete='fake'
className='argo-field'
style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}}
placeholder='Name'
value={item.name}
onChange={e => onChange({...item, name: e.target.value})}
// onBlur={e=>onChange({...item, name: e.target.value})}
title='Name'
readOnly={!onChange}
/>
&nbsp; = &nbsp;
<input
// disable chrome autocomplete
autoComplete='fake'
className='argo-field'
style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}}
placeholder='Value'
value={item.value || ''}
onChange={e => onChange({...item, value: e.target.value})}
title='Value'
readOnly={!onChange}
/>
&nbsp;
</React.Fragment>
);
};
export const ValueEditor = (item: string, onChange: (item: string) => any) => {
return (
<input
// disable chrome autocomplete
autoComplete='fake'
className='argo-field'
style={{width: '40%'}}
placeholder='Name'
value={item.name || ''}
onChange={e => onChange({...item, name: e.target.value})}
title='Name'
/>
&nbsp; = &nbsp;
<input
// disable chrome autocomplete
autoComplete='fake'
className='argo-field'
style={{width: '40%'}}
style={{width: '40%', borderColor: !onChange ? '#eff3f5' : undefined}}
placeholder='Value'
value={item.value || ''}
onChange={e => onChange({...item, value: e.target.value})}
value={item || ''}
onChange={e => onChange(e.target.value)}
title='Value'
readOnly={!onChange}
/>
&nbsp;
</React.Fragment>
);
);
};
interface Props<T> {
items: T[];
@@ -97,6 +119,50 @@ export function ArrayInput<T>(props: Props<T>) {
);
}
export const ResetOrDeleteButton = (props: {
isPluginPar: boolean;
getValue: () => FormValue;
name: string;
index: number;
setValue: (value: FormValue) => void;
setAppParamsDeletedState: any;
}) => {
const handleDeleteChange = () => {
if (props.index >= 0) {
props.setAppParamsDeletedState((val: string[]) => val.concat(props.name));
}
};
const handleResetChange = () => {
if (props.index >= 0) {
const items = [...props.getValue()];
items.splice(props.index, 1);
props.setValue(items);
}
};
const disabled = props.index === -1;
const content = props.isPluginPar ? 'Reset' : 'Delete';
let tooltip = '';
if (content === 'Reset' && !disabled) {
tooltip = 'Resets the parameter to the value provided by the plugin. This removes the parameter override from the application manifest';
} else if (content === 'Delete' && !disabled) {
tooltip = 'Deletes this parameter values from the application manifest.';
}
return (
<button
className='argo-button argo-button--base'
disabled={disabled}
title={tooltip}
style={{fontSize: '12px', display: 'flex', marginLeft: 'auto', marginTop: '8px'}}
onClick={props.isPluginPar ? handleResetChange : handleDeleteChange}>
{content}
</button>
);
};
export const ArrayInputField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => {
const {
fieldApi: {getValue, setValue}
@@ -104,6 +170,95 @@ export const ArrayInputField = ReactForm.FormField((props: {fieldApi: ReactForm.
return <ArrayInput editor={NameValueEditor} items={getValue() || []} onChange={setValue} />;
});
export const ArrayValueField = ReactForm.FormField(
(props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: string[]; isPluginPar: boolean; setAppParamsDeletedState: any}) => {
const {
fieldApi: {getValue, setValue}
} = props;
let liveParamArray;
const liveParam = getValue()?.find((val: {name: string; array: object}) => val.name === props.name);
if (liveParam) {
liveParamArray = liveParam?.array ?? [];
}
const index = getValue()?.findIndex((val: {name: string; array: object}) => val.name === props.name) ?? -1;
const values = liveParamArray ?? props.defaultVal ?? [];
return (
<React.Fragment>
<ResetOrDeleteButton
isPluginPar={props.isPluginPar}
getValue={getValue}
name={props.name}
index={index}
setValue={setValue}
setAppParamsDeletedState={props.setAppParamsDeletedState}
/>
<ArrayInput
editor={ValueEditor}
items={values || []}
onChange={change => {
const update = change.map((val: string | object) => (typeof val !== 'string' ? '' : val));
if (index >= 0) {
getValue()[index].array = update;
setValue([...getValue()]);
} else {
setValue([...(getValue() || []), {name: props.name, array: update}]);
}
}}
/>
</React.Fragment>
);
}
);
export const StringValueField = ReactForm.FormField(
(props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: string; isPluginPar: boolean; setAppParamsDeletedState: any}) => {
const {
fieldApi: {getValue, setValue}
} = props;
let liveParamString;
const liveParam = getValue()?.find((val: {name: string; string: string}) => val.name === props.name);
if (liveParam) {
liveParamString = liveParam?.string ? liveParam?.string : '';
}
const values = liveParamString ?? props.defaultVal ?? '';
const index = getValue()?.findIndex((val: {name: string; string: string}) => val.name === props.name) ?? -1;
return (
<React.Fragment>
<ResetOrDeleteButton
isPluginPar={props.isPluginPar}
getValue={getValue}
name={props.name}
index={index}
setValue={setValue}
setAppParamsDeletedState={props.setAppParamsDeletedState}
/>
<div>
<input
// disable chrome autocomplete
autoComplete='fake'
className='argo-field'
style={{width: '40%', display: 'inline-block', marginTop: 25}}
placeholder='Value'
value={values || ''}
onChange={e => {
if (index >= 0) {
getValue()[index].string = e.target.value;
setValue([...getValue()]);
} else {
setValue([...(getValue() || []), {name: props.name, string: e.target.value}]);
}
}}
title='Value'
/>
</div>
</React.Fragment>
);
}
);
export const MapInputField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => {
const {
fieldApi: {getValue, setValue}
@@ -123,3 +278,55 @@ export const MapInputField = ReactForm.FormField((props: {fieldApi: ReactForm.Fi
/>
);
});
export const MapValueField = ReactForm.FormField(
(props: {fieldApi: ReactForm.FieldApi; name: string; defaultVal: Map<string, string>; isPluginPar: boolean; setAppParamsDeletedState: any}) => {
const {
fieldApi: {getValue, setValue}
} = props;
const items = new Array<NameValue>();
const liveParam = getValue()?.find((val: {name: string; map: object}) => val.name === props.name);
const index = getValue()?.findIndex((val: {name: string; map: object}) => val.name === props.name) ?? -1;
if (liveParam) {
liveParam.map = liveParam.map ? liveParam.map : new Map<string, string>();
}
if (liveParam?.array) {
items.push(...liveParam.array);
} else {
const map = liveParam?.map ?? props.defaultVal ?? new Map<string, string>();
Object.keys(map).forEach(item => items.push({name: item || '', value: map[item] || ''}));
if (liveParam?.map) {
getValue()[index].array = items;
}
}
return (
<React.Fragment>
<ResetOrDeleteButton
isPluginPar={props.isPluginPar}
getValue={getValue}
name={props.name}
index={index}
setValue={setValue}
setAppParamsDeletedState={props.setAppParamsDeletedState}
/>
<ArrayInput
editor={NameValueEditor}
items={items || []}
onChange={change => {
if (index === -1) {
getValue().push({
name: props.name,
array: change
});
} else {
getValue()[index].array = change;
}
setValue([...getValue()]);
}}
/>
</React.Fragment>
);
}
);

View File

@@ -3,12 +3,12 @@ import * as classNames from 'classnames';
import * as React from 'react';
import {Form, FormApi} from 'react-form';
import {helpTip} from '../../../applications/components/utils';
import {Consumer} from '../../context';
import {Spinner} from '../spinner';
export interface EditablePanelItem {
title: string;
customTitle?: string | React.ReactNode;
key?: string;
before?: React.ReactNode;
view: string | React.ReactNode;
@@ -104,7 +104,7 @@ export class EditablePanel<T = {}> extends React.Component<EditablePanelProps<T>
<React.Fragment key={item.key || item.title}>
{item.before}
<div className='row white-box__details-row'>
<div className='columns small-3'>{item.title}</div>
<div className='columns small-3'>{item.customTitle || item.title}</div>
<div className='columns small-9'>{item.view}</div>
</div>
</React.Fragment>
@@ -142,7 +142,7 @@ export class EditablePanel<T = {}> extends React.Component<EditablePanelProps<T>
<React.Fragment key={item.key || item.title}>
{item.before}
<div className='row white-box__details-row'>
<div className='columns small-3'>{(item.titleEdit && item.titleEdit(api)) || item.title}</div>
<div className='columns small-3'>{(item.titleEdit && item.titleEdit(api)) || item.customTitle || item.title}</div>
<div className='columns small-9'>{(item.edit && item.edit(api)) || item.view}</div>
</div>
</React.Fragment>

View File

@@ -143,8 +143,13 @@ func matchRepositoryCMP(ctx context.Context, repoPath string, client pluginclien
}
func cmpSupports(ctx context.Context, pluginSockFilePath, repoPath, fileName string, env []string, tarExcludedGlobs []string, namedPlugin bool) (io.Closer, pluginclient.ConfigManagementPluginServiceClient, bool) {
address := filepath.Join(pluginSockFilePath, fileName)
if !files.Inbound(address, pluginSockFilePath) {
absPluginSockFilePath, err := filepath.Abs(pluginSockFilePath)
if err != nil {
log.Errorf("error getting absolute path for plugin socket dir %v, %v", pluginSockFilePath, err)
return nil, nil, false
}
address := filepath.Join(absPluginSockFilePath, fileName)
if !files.Inbound(address, absPluginSockFilePath) {
log.Errorf("invalid socket file path, %v is outside plugin socket dir %v", fileName, pluginSockFilePath)
return nil, nil, false
}