From f9d79b051f6e8b4d997aaf6020b3c82c403a38e9 Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 10 Oct 2022 16:23:00 +0300 Subject: [PATCH 01/38] Kind wildcard support in health customizations Signed-off-by: reggie --- util/lua/lua.go | 28 +++++++++++++++++++++++++++- util/lua/lua_test.go | 16 ++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/util/lua/lua.go b/util/lua/lua.go index 34d1caf0ca56a..95c35eb3c01da 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -122,13 +122,32 @@ func (vm VM) ExecuteHealthLua(obj *unstructured.Unstructured, script string) (*h // GetHealthScript attempts to read lua script from config and then filesystem for that resource func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, bool, error) { + // first, search the gvk as is in the ResourceOverrides key := GetConfigMapKey(obj.GroupVersionKind()) + if script, ok := vm.ResourceOverrides[key]; ok && script.HealthLua != "" { return script.HealthLua, script.UseOpenLibs, nil } + + // if not found as is, search it with a Kind wildcard in the ResourceOverrides + kindWildcardKey := GetKindWildcardConfigMapKey(obj.GroupVersionKind()) + + if wildcardScript, ok := vm.ResourceOverrides[kindWildcardKey]; ok && wildcardScript.HealthLua != "" { + return wildcardScript.HealthLua, wildcardScript.UseOpenLibs, nil + } + + // if not found, search it as is in the built-in scripts builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile) + if builtInScript != "" && err == nil { + // standard libraries will be enabled for all built-in scripts + return builtInScript, true, nil + } + + // finally, search it with a Kind wildcard in the built-in scripts + builtInKindWildcardScript, err := vm.getPredefinedLuaScripts(kindWildcardKey, healthScriptFile) // standard libraries will be enabled for all built-in scripts - return builtInScript, true, err + return builtInKindWildcardScript, true, err + } func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string) (*unstructured.Unstructured, error) { @@ -333,6 +352,13 @@ func GetConfigMapKey(gvk schema.GroupVersionKind) string { return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) } +func GetKindWildcardConfigMapKey(gvk schema.GroupVersionKind) string { + if gvk.Group == "" { + return "*" + } + return fmt.Sprintf("%s/%s", gvk.Group, "*") +} + func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string, error) { data, err := resource_customizations.Embedded.ReadFile(filepath.Join(objKey, scriptFile)) if err != nil { diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index 628db49db8284..fe67a3290eee9 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -126,6 +126,22 @@ func TestGetHealthScriptWithOverride(t *testing.T) { assert.Equal(t, newHealthStatusFunction, script) } +func TestGetHealthScriptWithKindWildcardOverride(t *testing.T) { + testObj := StrToUnstructured(objJSON) + vm := VM{ + ResourceOverrides: map[string]appv1.ResourceOverride{ + "argoproj.io/*": { + HealthLua: newHealthStatusFunction, + UseOpenLibs: false, + }, + }, + } + script, useOpenLibs, err := vm.GetHealthScript(testObj) + assert.Nil(t, err) + assert.Equal(t, false, useOpenLibs) + assert.Equal(t, newHealthStatusFunction, script) +} + func TestGetHealthScriptPredefined(t *testing.T) { testObj := StrToUnstructured(objJSON) vm := VM{} From aff1d44760d93f4f2ac68897cb6e725c6da0c995 Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 10 Oct 2022 18:01:14 +0300 Subject: [PATCH 02/38] Updated health customizations docs to using the correct field with a / Signed-off-by: reggie --- docs/operator-manual/health.md | 46 +++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index 947a2753f1033..b55e8d9c088a4 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -62,34 +62,40 @@ There are two ways to configure a custom health check. The next two sections des ### Way 1. Define a Custom Health Check in `argocd-cm` ConfigMap -Custom health checks can be defined in `resource.customizations.health.` field of `argocd-cm`. If you are using argocd-operator, this is overridden by [the argocd-operator resourceCustomizations](https://argocd-operator.readthedocs.io/en/latest/reference/argocd/#resource-customizations). +Custom health checks can be defined in ` + resource.customizations: | + : + health.lua: | ` +field of `argocd-cm`. If you are using argocd-operator, this is overridden by [the argocd-operator resourceCustomizations](https://argocd-operator.readthedocs.io/en/latest/reference/argocd/#resource-customizations). The following example demonstrates a health check for `cert-manager.io/Certificate`. ```yaml data: - resource.customizations.health.cert-manager.io_Certificate: | - hs = {} - if obj.status ~= nil then - if obj.status.conditions ~= nil then - for i, condition in ipairs(obj.status.conditions) do - if condition.type == "Ready" and condition.status == "False" then - hs.status = "Degraded" - hs.message = condition.message - return hs - end - if condition.type == "Ready" and condition.status == "True" then - hs.status = "Healthy" - hs.message = condition.message - return hs + resource.customizations: | + cert-manager.io/Certificate: + health.lua: | + hs = {} + if obj.status ~= nil then + if obj.status.conditions ~= nil then + for i, condition in ipairs(obj.status.conditions) do + if condition.type == "Ready" and condition.status == "False" then + hs.status = "Degraded" + hs.message = condition.message + return hs + end + if condition.type == "Ready" and condition.status == "True" then + hs.status = "Healthy" + hs.message = condition.message + return hs + end + end end end - end - end - hs.status = "Progressing" - hs.message = "Waiting for certificate" - return hs + hs.status = "Progressing" + hs.message = "Waiting for certificate" + return hs ``` The `obj` is a global variable which contains the resource. The script must return an object with status and optional message field. From 0654777c2669162fcefda2d7d9b2cf4af39f5175 Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 10 Oct 2022 18:09:38 +0300 Subject: [PATCH 03/38] Updated health customizations docs to using the correct field with a / Signed-off-by: reggie --- docs/operator-manual/health.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index b55e8d9c088a4..be324ddd129f2 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -36,19 +36,21 @@ metadata: app.kubernetes.io/name: argocd-cm app.kubernetes.io/part-of: argocd data: - resource.customizations.health.argoproj.io_Application: | - hs = {} - hs.status = "Progressing" - hs.message = "" - if obj.status ~= nil then - if obj.status.health ~= nil then - hs.status = obj.status.health.status - if obj.status.health.message ~= nil then - hs.message = obj.status.health.message + resource.customizations: | + argoproj.io/Application: + health.lua: | + hs = {} + hs.status = "Progressing" + hs.message = "" + if obj.status ~= nil then + if obj.status.health ~= nil then + hs.status = obj.status.health.status + if obj.status.health.message ~= nil then + hs.message = obj.status.health.message + end + end end - end - end - return hs + return hs ``` ## Custom Health Checks @@ -62,10 +64,12 @@ There are two ways to configure a custom health check. The next two sections des ### Way 1. Define a Custom Health Check in `argocd-cm` ConfigMap -Custom health checks can be defined in ` +Custom health checks can be defined in +```yaml resource.customizations: | : - health.lua: | ` + health.lua: | +``` field of `argocd-cm`. If you are using argocd-operator, this is overridden by [the argocd-operator resourceCustomizations](https://argocd-operator.readthedocs.io/en/latest/reference/argocd/#resource-customizations). The following example demonstrates a health check for `cert-manager.io/Certificate`. From eedcd092a50d8ebbf91c9df08df4854b2f12faf5 Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 10 Oct 2022 18:17:00 +0300 Subject: [PATCH 04/38] Document resource kind wildcard for custom health check Signed-off-by: reggie --- docs/operator-manual/health.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index be324ddd129f2..325da33cb3da3 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -101,6 +101,14 @@ data: hs.message = "Waiting for certificate" return hs ``` +In order to prevent duplication of the same custom health check for potentially multiple resources, it is also possible to specify a wildcard in the resource kind, like this: + +```yaml + resource.customizations: | + ec2.aws.crossplane.io/*: + health.lua: | + ... +``` The `obj` is a global variable which contains the resource. The script must return an object with status and optional message field. The custom health check might return one of the following health statuses: From e98d753b18121e984c5af88b0348fa474ad88fbc Mon Sep 17 00:00:00 2001 From: reggie Date: Wed, 12 Oct 2022 18:34:15 +0300 Subject: [PATCH 05/38] Implemented wildcard * support in API Group and Resource Kind and updated docs Signed-off-by: reggie --- docs/operator-manual/health.md | 2 ++ util/lua/lua.go | 42 +++++++++++++++++++++------------- util/lua/lua_test.go | 18 +++++++++++++++ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index 325da33cb3da3..37a253b26efe0 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -157,3 +157,5 @@ tests: To test the implemented custom health checks, run `go test -v ./util/lua/`. The [PR#1139](https://github.com/argoproj/argo-cd/pull/1139) is an example of Cert Manager CRDs custom health check. + +Please note that bundled health checks with wildcards are not suported. diff --git a/util/lua/lua.go b/util/lua/lua.go index 95c35eb3c01da..b0b05b805e24b 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -16,6 +16,7 @@ import ( appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/resource_customizations" + "github.com/argoproj/argo-cd/v2/util/glob" ) const ( @@ -129,25 +130,18 @@ func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, bool, erro return script.HealthLua, script.UseOpenLibs, nil } - // if not found as is, search it with a Kind wildcard in the ResourceOverrides - kindWildcardKey := GetKindWildcardConfigMapKey(obj.GroupVersionKind()) + // if not found as is, perhaps it matches wildcard entries in the configmap + wildcardKey := GetWildcardConfigMapKey(vm, obj.GroupVersionKind()) - if wildcardScript, ok := vm.ResourceOverrides[kindWildcardKey]; ok && wildcardScript.HealthLua != "" { + if wildcardScript, ok := vm.ResourceOverrides[wildcardKey]; ok && wildcardScript.HealthLua != "" { return wildcardScript.HealthLua, wildcardScript.UseOpenLibs, nil } - // if not found, search it as is in the built-in scripts + // if not found in the ResourceOverrides at all, search it as is in the built-in scripts + // (as built-in scripts are files in folders, named after the GVK, currently there is no wildcard support for them) builtInScript, err := vm.getPredefinedLuaScripts(key, healthScriptFile) - if builtInScript != "" && err == nil { - // standard libraries will be enabled for all built-in scripts - return builtInScript, true, nil - } - - // finally, search it with a Kind wildcard in the built-in scripts - builtInKindWildcardScript, err := vm.getPredefinedLuaScripts(kindWildcardKey, healthScriptFile) // standard libraries will be enabled for all built-in scripts - return builtInKindWildcardScript, true, err - + return builtInScript, true, err } func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string) (*unstructured.Unstructured, error) { @@ -352,13 +346,29 @@ func GetConfigMapKey(gvk schema.GroupVersionKind) string { return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) } -func GetKindWildcardConfigMapKey(gvk schema.GroupVersionKind) string { +func GetWildcardConfigMapKey(vm VM, gvk schema.GroupVersionKind) string { + var gvkKeyToMatch string + if gvk.Group == "" { - return "*" + gvkKeyToMatch = gvk.Kind + } else { + gvkKeyToMatch = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) + } + + for key := range vm.ResourceOverrides { + if glob.Match(key, gvkKeyToMatch) { + return key + } } - return fmt.Sprintf("%s/%s", gvk.Group, "*") + return gvkKeyToMatch } +func GetGroupWildcardConfigMapKey(gvk schema.GroupVersionKind) string { + if gvk.Group == "" { + return gvk.Kind + } + return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) +} func (vm VM) getPredefinedLuaScripts(objKey string, scriptFile string) (string, error) { data, err := resource_customizations.Embedded.ReadFile(filepath.Join(objKey, scriptFile)) if err != nil { diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index fe67a3290eee9..314f9a8ab313c 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -136,6 +136,24 @@ func TestGetHealthScriptWithKindWildcardOverride(t *testing.T) { }, }, } + + script, useOpenLibs, err := vm.GetHealthScript(testObj) + assert.Nil(t, err) + assert.Equal(t, false, useOpenLibs) + assert.Equal(t, newHealthStatusFunction, script) +} + +func TestGetHealthScriptWithGroupWildcardOverride(t *testing.T) { + testObj := StrToUnstructured(objJSON) + vm := VM{ + ResourceOverrides: map[string]appv1.ResourceOverride{ + "*.io/Rollout": { + HealthLua: newHealthStatusFunction, + UseOpenLibs: false, + }, + }, + } + script, useOpenLibs, err := vm.GetHealthScript(testObj) assert.Nil(t, err) assert.Equal(t, false, useOpenLibs) From 1117332efc77aa632b7c215d1a3f10710b3717f9 Mon Sep 17 00:00:00 2001 From: reggie Date: Wed, 12 Oct 2022 18:43:09 +0300 Subject: [PATCH 06/38] Implemented wildcard * support in API Group and Resource Kind and updated docs Signed-off-by: reggie --- docs/operator-manual/health.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/operator-manual/health.md b/docs/operator-manual/health.md index 37a253b26efe0..f52cfd27d73ee 100644 --- a/docs/operator-manual/health.md +++ b/docs/operator-manual/health.md @@ -158,4 +158,4 @@ To test the implemented custom health checks, run `go test -v ./util/lua/`. The [PR#1139](https://github.com/argoproj/argo-cd/pull/1139) is an example of Cert Manager CRDs custom health check. -Please note that bundled health checks with wildcards are not suported. +Please note that bundled health checks with wildcards are not supported. From 28d7d31e1ec5662cca56c48494894c980bbd9d93 Mon Sep 17 00:00:00 2001 From: reggie Date: Wed, 12 Oct 2022 18:58:10 +0300 Subject: [PATCH 07/38] Implemented wildcard * support in API Group and Resource Kind and updated docs Signed-off-by: reggie --- util/lua/lua.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/util/lua/lua.go b/util/lua/lua.go index b0b05b805e24b..1faea5815c411 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -3,6 +3,7 @@ package lua import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -131,10 +132,12 @@ func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, bool, erro } // if not found as is, perhaps it matches wildcard entries in the configmap - wildcardKey := GetWildcardConfigMapKey(vm, obj.GroupVersionKind()) + wildcardKey, err := GetWildcardConfigMapKey(vm, obj.GroupVersionKind()) - if wildcardScript, ok := vm.ResourceOverrides[wildcardKey]; ok && wildcardScript.HealthLua != "" { - return wildcardScript.HealthLua, wildcardScript.UseOpenLibs, nil + if err == nil { + if wildcardScript, ok := vm.ResourceOverrides[wildcardKey]; ok && wildcardScript.HealthLua != "" { + return wildcardScript.HealthLua, wildcardScript.UseOpenLibs, nil + } } // if not found in the ResourceOverrides at all, search it as is in the built-in scripts @@ -346,7 +349,7 @@ func GetConfigMapKey(gvk schema.GroupVersionKind) string { return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) } -func GetWildcardConfigMapKey(vm VM, gvk schema.GroupVersionKind) string { +func GetWildcardConfigMapKey(vm VM, gvk schema.GroupVersionKind) (string, error) { var gvkKeyToMatch string if gvk.Group == "" { @@ -357,10 +360,10 @@ func GetWildcardConfigMapKey(vm VM, gvk schema.GroupVersionKind) string { for key := range vm.ResourceOverrides { if glob.Match(key, gvkKeyToMatch) { - return key + return key, nil } } - return gvkKeyToMatch + return "", errors.New("No key found") } func GetGroupWildcardConfigMapKey(gvk schema.GroupVersionKind) string { From 390c71a12f8acba1dfe4c3af6d241d8f96f25f87 Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 14 Oct 2022 13:05:53 +0300 Subject: [PATCH 08/38] Added a custom create-from CronJob action Signed-off-by: reggie --- .../batch/CronJob/actions/action_test.yaml | 4 ++++ .../CronJob/actions/create-from/action.lua | 2 ++ .../batch/CronJob/actions/discovery.lua | 3 +++ .../actions/testdata/cronjob-altered.yaml | 22 +++++++++++++++++++ .../CronJob/actions/testdata/cronjob.yaml | 22 +++++++++++++++++++ 5 files changed, 53 insertions(+) create mode 100644 resource_customizations/batch/CronJob/actions/action_test.yaml create mode 100644 resource_customizations/batch/CronJob/actions/create-from/action.lua create mode 100644 resource_customizations/batch/CronJob/actions/discovery.lua create mode 100644 resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml create mode 100644 resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml diff --git a/resource_customizations/batch/CronJob/actions/action_test.yaml b/resource_customizations/batch/CronJob/actions/action_test.yaml new file mode 100644 index 0000000000000..86f23c5a529d3 --- /dev/null +++ b/resource_customizations/batch/CronJob/actions/action_test.yaml @@ -0,0 +1,4 @@ +actionTests: +- action: create-from + inputPath: testdata/cronjob.yaml + expectedOutputPath: testdata/cronjob-altered.yaml diff --git a/resource_customizations/batch/CronJob/actions/create-from/action.lua b/resource_customizations/batch/CronJob/actions/create-from/action.lua new file mode 100644 index 0000000000000..fc10822a21760 --- /dev/null +++ b/resource_customizations/batch/CronJob/actions/create-from/action.lua @@ -0,0 +1,2 @@ +obj.metadata.annotations["kuku"] = "muku" +return obj diff --git a/resource_customizations/batch/CronJob/actions/discovery.lua b/resource_customizations/batch/CronJob/actions/discovery.lua new file mode 100644 index 0000000000000..a44f35a606f6f --- /dev/null +++ b/resource_customizations/batch/CronJob/actions/discovery.lua @@ -0,0 +1,3 @@ +actions = {} +actions["create-from"] = {} +return actions \ No newline at end of file diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml new file mode 100644 index 0000000000000..d92fcef09a738 --- /dev/null +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml @@ -0,0 +1,22 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + annotations: + gu: gu + kuku: muku +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure \ No newline at end of file diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml new file mode 100644 index 0000000000000..1913c9f4edc44 --- /dev/null +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml @@ -0,0 +1,22 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + annotations: + gu: gu + kuku: ttt +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure \ No newline at end of file From 76be5a6c64e7bb0512b728e153fc2186f879be93 Mon Sep 17 00:00:00 2001 From: reggie Date: Wed, 1 Feb 2023 22:45:21 +0200 Subject: [PATCH 09/38] in progress Signed-off-by: reggie --- pkg/apis/application/v1alpha1/types.go | 13 ++++ server/application/application.go | 87 +++++++++++++------------- util/lua/custom_actions_test.go | 36 ++++++++--- util/lua/lua.go | 24 ++++++- 4 files changed, 105 insertions(+), 55 deletions(-) diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index 409489b14bb67..d386b7ce40f36 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -2651,6 +2651,19 @@ func UnmarshalToUnstructured(resource string) (*unstructured.Unstructured, error return &obj, nil } +// UnmarshalToUnstructuredList unmarshals a resource list representation in JSON to unstructured data list +func UnmarshalToUnstructuredList(listResource string) (*unstructured.UnstructuredList, error) { + if listResource == "" || listResource == "null" { + return nil, nil + } + var obj unstructured.UnstructuredList + err := json.Unmarshal([]byte(listResource), &obj) + if err != nil { + return nil, err + } + return &obj, nil +} + // TODO: document this method func (r ResourceDiff) LiveObject() (*unstructured.Unstructured, error) { return UnmarshalToUnstructured(r.LiveState) diff --git a/server/application/application.go b/server/application/application.go index dbdce9cafce2e..ab11aa58479b6 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2151,58 +2151,61 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error getting Lua resource action: %w", err) } - newObj, err := luaVM.ExecuteResourceAction(liveObj, action.ActionLua) + newObjects, err := luaVM.ExecuteResourceAction(liveObj, action.ActionLua) if err != nil { return nil, fmt.Errorf("error executing Lua resource action: %w", err) } - newObjBytes, err := json.Marshal(newObj) - if err != nil { - return nil, fmt.Errorf("error marshaling new object: %w", err) - } + for _, impactedResource := range newObjects { + newObj := impactedResource.UnstructuredObj + newObjBytes, err := json.Marshal(newObj) + if err != nil { + return nil, fmt.Errorf("error marshaling new object: %w", err) + } - liveObjBytes, err := json.Marshal(liveObj) - if err != nil { - return nil, fmt.Errorf("error marshaling live object: %w", err) - } + liveObjBytes, err := json.Marshal(liveObj) + if err != nil { + return nil, fmt.Errorf("error marshaling live object: %w", err) + } - diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) - if err != nil { - return nil, fmt.Errorf("error calculating merge patch: %w", err) - } - if string(diffBytes) == "{}" { - return &application.ApplicationResponse{}, nil - } + diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) + if err != nil { + return nil, fmt.Errorf("error calculating merge patch: %w", err) + } + if string(diffBytes) == "{}" { + return &application.ApplicationResponse{}, nil + } - // The following logic detects if the resource action makes a modification to status and/or spec. - // If status was modified, we attempt to patch the status using status subresource, in case the - // CRD is configured using the status subresource feature. See: - // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource - // If status subresource is in use, the patch has to be split into two: - // * one to update spec (and other non-status fields) - // * the other to update only status. - nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) - if err != nil { - return nil, fmt.Errorf("error splitting status patch: %w", err) - } - if statusPatch != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") + // The following logic detects if the resource action makes a modification to status and/or spec. + // If status was modified, we attempt to patch the status using status subresource, in case the + // CRD is configured using the status subresource feature. See: + // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource + // If status subresource is in use, the patch has to be split into two: + // * one to update spec (and other non-status fields) + // * the other to update only status. + nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) if err != nil { - if !apierr.IsNotFound(err) { - return nil, fmt.Errorf("error patching resource: %w", err) + return nil, fmt.Errorf("error splitting status patch: %w", err) + } + if statusPatch != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") + if err != nil { + if !apierr.IsNotFound(err) { + return nil, fmt.Errorf("error patching resource: %w", err) + } + // K8s API server returns 404 NotFound when the CRD does not support the status subresource + // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch + } else { + // If we get here, the CRD does use the status subresource, so we must patch status and + // spec separately. update the diffBytes to the spec-only patch and fall through. + diffBytes = nonStatusPatch } - // K8s API server returns 404 NotFound when the CRD does not support the status subresource - // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch - } else { - // If we get here, the CRD does use the status subresource, so we must patch status and - // spec separately. update the diffBytes to the spec-only patch and fall through. - diffBytes = nonStatusPatch } - } - if diffBytes != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) - if err != nil { - return nil, fmt.Errorf("error patching resource: %w", err) + if diffBytes != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) + if err != nil { + return nil, fmt.Errorf("error patching resource: %w", err) + } } } diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index e5a10700e181b..1cca927dc3690 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -40,7 +40,7 @@ type IndividualActionTest struct { } func TestLuaResourceActionsScript(t *testing.T) { - err := filepath.Walk(".", func(path string, f os.FileInfo, err error) error { + err := filepath.Walk("../../resource_customizations", func(path string, f os.FileInfo, err error) error { if !strings.Contains(path, "action_test.yaml") { return nil } @@ -84,18 +84,34 @@ func TestLuaResourceActionsScript(t *testing.T) { assert.NoError(t, err) assert.NoError(t, err) - result, err := vm.ExecuteResourceAction(obj, action.ActionLua) + results, err := vm.ExecuteResourceAction(obj, action.ActionLua) assert.NoError(t, err) - expectedObj := getObj(filepath.Join(dir, test.ExpectedOutputPath)) - // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. - diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) - assert.NoError(t, err) - if diffResult.Modified { - t.Error("Output does not match input:") - err = cli.PrintDiff(test.Action, expectedObj, result) - assert.NoError(t, err) + for _, impactedResource := range results { + result := impactedResource.UnstructuredObj + if impactedResource.K8SOperation == "patch" { + // Patching is only allowed for the source resource, so the result must have exactly 1 resource + assert.EqualValues(t, 1, len(results)) + // Patching is only allowed for the source resource, so the kind must be the same + assert.EqualValues(t, obj.GetKind(), impactedResource.UnstructuredObj.GetKind()) + + expectedObj := getObj(filepath.Join(dir, test.ExpectedOutputPath)) + // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. + diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) + assert.NoError(t, err) + if diffResult.Modified { + t.Error("Output does not match input:") + err = cli.PrintDiff(test.Action, expectedObj, result) + assert.NoError(t, err) + } + } else { + if impactedResource.K8SOperation == "create" { + t.Error("There is a create action - hooray! But I don't know ho to test it yet") + } + + } } + }) } diff --git a/util/lua/lua.go b/util/lua/lua.go index ad2375bd939b2..13a1462eecf38 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -3,7 +3,6 @@ package lua import ( "context" "encoding/json" - "errors" "fmt" "os" "path/filepath" @@ -57,6 +56,15 @@ type VM struct { UseOpenLibs bool } +// This struct represents a resource, that is returned from Lua custom action script, along with a k8s verb +// that will need to be performed on this returned resource. +// This replaces the traditional architecture of "Lua action returns a resource that ArgoCD will patch". +// This enables ArgoCD to create NEW resources upon custom actions. +type ImpactedResource struct { + UnstructuredObj *unstructured.Unstructured + K8SOperation string +} + func (vm VM) runLua(obj *unstructured.Unstructured, script string) (*lua.LState, error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, @@ -147,7 +155,7 @@ func (vm VM) GetHealthScript(obj *unstructured.Unstructured) (string, bool, erro return builtInScript, true, err } -func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string) (*unstructured.Unstructured, error) { +func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string) ([]ImpactedResource, error) { l, err := vm.runLua(obj, script) if err != nil { return nil, err @@ -164,7 +172,17 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string } cleanedNewObj := cleanReturnedObj(newObj.Object, obj.Object) newObj.Object = cleanedNewObj - return newObj, nil + + // TODO: delete this hard coded thingie when Lua script returns an array of objects along with corresponding actions. + // Meanwhile, just wrapping Job resource with a "create" operation, and wrapping everything else with a "patch" operation. + // The "everything else" are traditional outputs from a ResourceAction - the update to the same resource the action is invoked on. + impactedResources := make([]ImpactedResource, 0) + if newObj.GetKind() == "Job" { + impactedResources = append(impactedResources, ImpactedResource{newObj, "create"}) + } else { + impactedResources = append(impactedResources, ImpactedResource{newObj, "patch"}) + } + return impactedResources, nil } return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String()) } From 2e72471aa849ee62fb77bd34f09dd8a172fbabed Mon Sep 17 00:00:00 2001 From: reggie Date: Wed, 1 Feb 2023 22:50:43 +0200 Subject: [PATCH 10/38] in progress Signed-off-by: reggie --- cmd/argocd/commands/admin/settings.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index af758067b53d9..30026e0d27bdf 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -545,13 +545,23 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a modifiedRes, err := luaVM.ExecuteResourceAction(&res, action.ActionLua) errors.CheckError(err) - if reflect.DeepEqual(&res, modifiedRes) { - _, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name) - return - } + for _, impactedResource := range modifiedRes { + result := impactedResource.UnstructuredObj + if impactedResource.K8SOperation == "patch" { + if reflect.DeepEqual(&res, modifiedRes) { + _, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name) + return + } + + _, _ = fmt.Printf("Following fields have been changed:\n\n") + _ = cli.PrintDiff(res.GetName(), &res, result) + } else { + if impactedResource.K8SOperation == "create" { + _, _ = fmt.Printf("Create action detected. Don't know what to print yet") + } + } - _, _ = fmt.Printf("Following fields have been changed:\n\n") - _ = cli.PrintDiff(res.GetName(), &res, modifiedRes) + } }) }, } From 6c9cf61c789c92052f1714924c0857e3e7c33a8b Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 10 Feb 2023 22:46:44 +0200 Subject: [PATCH 11/38] in progress Signed-off-by: reggie --- server/application/application.go | 97 +++++++++++++------ server/application/application_test.go | 37 +++++++ .../testdata/resource-actions/cron-job.yaml | 20 ++++ .../testdata/resource-actions/cron-job.yaml | 20 ++++ util/lua/lua.go | 4 + 5 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 server/application/testdata/resource-actions/cron-job.yaml create mode 100644 test/e2e/testdata/resource-actions/cron-job.yaml diff --git a/server/application/application.go b/server/application/application.go index ab11aa58479b6..5766771727235 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes" @@ -2123,6 +2124,12 @@ func (s *Server) getAvailableActions(resourceOverrides map[string]appv1.Resource } func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceActionRunRequest) (*application.ApplicationResponse, error) { + appName := q.GetName() + appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) + app, err := s.appLister.Applications(appNs).Get(appName) + if err != nil { + return nil, fmt.Errorf("error getting application: %w", err) + } resourceRequest := &application.ApplicationResourceRequest{ Name: q.Name, AppNamespace: q.AppNamespace, @@ -2168,43 +2175,69 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error marshaling live object: %w", err) } - diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) - if err != nil { - return nil, fmt.Errorf("error calculating merge patch: %w", err) - } - if string(diffBytes) == "{}" { - return &application.ApplicationResponse{}, nil - } + if impactedResource.K8SOperation == "patch" { - // The following logic detects if the resource action makes a modification to status and/or spec. - // If status was modified, we attempt to patch the status using status subresource, in case the - // CRD is configured using the status subresource feature. See: - // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource - // If status subresource is in use, the patch has to be split into two: - // * one to update spec (and other non-status fields) - // * the other to update only status. - nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) - if err != nil { - return nil, fmt.Errorf("error splitting status patch: %w", err) - } - if statusPatch != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") + diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) + if err != nil { + return nil, fmt.Errorf("error calculating merge patch: %w", err) + } + if string(diffBytes) == "{}" { + return &application.ApplicationResponse{}, nil + } + + // The following logic detects if the resource action makes a modification to status and/or spec. + // If status was modified, we attempt to patch the status using status subresource, in case the + // CRD is configured using the status subresource feature. See: + // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource + // If status subresource is in use, the patch has to be split into two: + // * one to update spec (and other non-status fields) + // * the other to update only status. + nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) if err != nil { - if !apierr.IsNotFound(err) { + return nil, fmt.Errorf("error splitting status patch: %w", err) + } + if statusPatch != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") + if err != nil { + if !apierr.IsNotFound(err) { + return nil, fmt.Errorf("error patching resource: %w", err) + } + // K8s API server returns 404 NotFound when the CRD does not support the status subresource + // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch + } else { + // If we get here, the CRD does use the status subresource, so we must patch status and + // spec separately. update the diffBytes to the spec-only patch and fall through. + diffBytes = nonStatusPatch + } + } + if diffBytes != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) + if err != nil { return nil, fmt.Errorf("error patching resource: %w", err) } - // K8s API server returns 404 NotFound when the CRD does not support the status subresource - // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch - } else { - // If we get here, the CRD does use the status subresource, so we must patch status and - // spec separately. update the diffBytes to the spec-only patch and fall through. - diffBytes = nonStatusPatch } - } - if diffBytes != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) - if err != nil { - return nil, fmt.Errorf("error patching resource: %w", err) + } else { + if impactedResource.K8SOperation == "create" { + proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) + if err != nil { + if apierr.IsNotFound(err) { + return nil, fmt.Errorf("application references project %s which does not exist", app.Spec.Project) + } + } + permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: newObj.GroupVersionKind().Group, Kind: newObj.GroupVersionKind().Kind}, newObj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { + clusters, err := s.db.GetProjectClusters(context.TODO(), project) + if err != nil { + return nil, fmt.Errorf("failed to get project clusters: %w", err) + } + return clusters, nil + }) + if err != nil { + return nil, fmt.Errorf("error checking resource permissions: %w", err) + } + if !permitted { + return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) + } + // Create the resource } } } diff --git a/server/application/application_test.go b/server/application/application_test.go index 7569734d33b42..a7d30fa92fa17 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1145,3 +1145,40 @@ func TestInferResourcesStatusHealth(t *testing.T) { assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) assert.Nil(t, testApp.Status.Resources[1].Health) } + +func TestRunResourceAction_WithCreateOperationPermitted(t *testing.T) { + testApp := newTestApp() + appServer := newTestAppServer(testApp) + + group := "batch" + kind := "CronJob" + version := "v1" + resourceName := "my-cron-job" + namespace := "kuku" + action := "create-from" + + _, err := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + assert.NoError(t, err) +} + +func TestRunResourceAction_WithCreateOperationDenied(t *testing.T) { + +} + +func TestRunResourceAction_WithUpdateOperationPermitted(t *testing.T) { + +} + +func TestRunResourceAction_WithUpdateOperationDenied(t *testing.T) { + +} diff --git a/server/application/testdata/resource-actions/cron-job.yaml b/server/application/testdata/resource-actions/cron-job.yaml new file mode 100644 index 0000000000000..58f56de94232e --- /dev/null +++ b/server/application/testdata/resource-actions/cron-job.yaml @@ -0,0 +1,20 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure \ No newline at end of file diff --git a/test/e2e/testdata/resource-actions/cron-job.yaml b/test/e2e/testdata/resource-actions/cron-job.yaml new file mode 100644 index 0000000000000..58f56de94232e --- /dev/null +++ b/test/e2e/testdata/resource-actions/cron-job.yaml @@ -0,0 +1,20 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure \ No newline at end of file diff --git a/util/lua/lua.go b/util/lua/lua.go index 13a1462eecf38..8fb00dd0cc569 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -176,6 +176,10 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string // TODO: delete this hard coded thingie when Lua script returns an array of objects along with corresponding actions. // Meanwhile, just wrapping Job resource with a "create" operation, and wrapping everything else with a "patch" operation. // The "everything else" are traditional outputs from a ResourceAction - the update to the same resource the action is invoked on. + // The logic, that goes over the array returned from Lua, and makes sure that: + // - there is at most 1 patch operation + // - if such patch operation exists, it is being invoked only on the source resource + // will be somewhere here, and if one those is violated, an error will ne thrown impactedResources := make([]ImpactedResource, 0) if newObj.GetKind() == "Job" { impactedResources = append(impactedResources, ImpactedResource{newObj, "create"}) From 581d0897022d123d03d65b2cd0a232e0c5fc8604 Mon Sep 17 00:00:00 2001 From: reggie Date: Thu, 23 Feb 2023 18:54:12 +0200 Subject: [PATCH 12/38] in progress Signed-off-by: reggie --- .../batch/CronJob/actions/action_test.yaml | 2 +- .../CronJob/actions/create-from/action.lua | 19 ++- .../actions/testdata/cronjob-altered.yaml | 22 ---- .../CronJob/actions/testdata/cronjob.yaml | 3 - .../batch/CronJob/actions/testdata/job.yaml | 8 +- server/application/application.go | 1 + server/application/application_test.go | 119 +++++++++++++----- util/lua/custom_actions_test.go | 10 +- util/lua/lua.go | 2 +- 9 files changed, 119 insertions(+), 67 deletions(-) delete mode 100644 resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml rename server/application/testdata/resource-actions/cron-job.yaml => resource_customizations/batch/CronJob/actions/testdata/job.yaml (79%) diff --git a/resource_customizations/batch/CronJob/actions/action_test.yaml b/resource_customizations/batch/CronJob/actions/action_test.yaml index 86f23c5a529d3..cea31e7aabc15 100644 --- a/resource_customizations/batch/CronJob/actions/action_test.yaml +++ b/resource_customizations/batch/CronJob/actions/action_test.yaml @@ -1,4 +1,4 @@ actionTests: - action: create-from inputPath: testdata/cronjob.yaml - expectedOutputPath: testdata/cronjob-altered.yaml + expectedOutputPath: testdata/job.yaml diff --git a/resource_customizations/batch/CronJob/actions/create-from/action.lua b/resource_customizations/batch/CronJob/actions/create-from/action.lua index fc10822a21760..b317d8a01f824 100644 --- a/resource_customizations/batch/CronJob/actions/create-from/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-from/action.lua @@ -1,2 +1,17 @@ -obj.metadata.annotations["kuku"] = "muku" -return obj +local os = require("os") +job = {} +job.apiVersion = "batch/v1" +job.kind = "Job" +job.metadata = {} +job.metadata.name = obj.metadata.name .. os.date("!%Y%m%d%H%M") +job.spec = {} +job.spec.template = {} +job.spec.template.spec = obj.spec.jobTemplate.spec.template.spec +job.metadata.ownerReferences = {} +ownerRef = {} +ownerRef.apiVersion = obj.apiVersion +ownerRef.kind = "CronJob" +ownerRef.name = obj.metadata.name +ownerRef.uid = obj.metadata.uid +-- job.metadata.ownerReferences[0] = ownerRef +return job \ No newline at end of file diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml deleted file mode 100644 index d92fcef09a738..0000000000000 --- a/resource_customizations/batch/CronJob/actions/testdata/cronjob-altered.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: batch/v1 -kind: CronJob -metadata: - name: hello - annotations: - gu: gu - kuku: muku -spec: - schedule: "* * * * *" - jobTemplate: - spec: - template: - spec: - containers: - - name: hello - image: busybox:1.28 - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -c - - date; echo Hello from the Kubernetes cluster - restartPolicy: OnFailure \ No newline at end of file diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml index 1913c9f4edc44..3ab1fb9b1cd8a 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml @@ -2,9 +2,6 @@ apiVersion: batch/v1 kind: CronJob metadata: name: hello - annotations: - gu: gu - kuku: ttt spec: schedule: "* * * * *" jobTemplate: diff --git a/server/application/testdata/resource-actions/cron-job.yaml b/resource_customizations/batch/CronJob/actions/testdata/job.yaml similarity index 79% rename from server/application/testdata/resource-actions/cron-job.yaml rename to resource_customizations/batch/CronJob/actions/testdata/job.yaml index 58f56de94232e..bc7235b297b36 100644 --- a/server/application/testdata/resource-actions/cron-job.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/job.yaml @@ -1,12 +1,8 @@ apiVersion: batch/v1 -kind: CronJob +kind: Job metadata: - name: hello - namespace: + name: hello-123 spec: - schedule: "* * * * *" - jobTemplate: - spec: template: spec: containers: diff --git a/server/application/application.go b/server/application/application.go index 5766771727235..3cfa15512bcf6 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2238,6 +2238,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) } // Create the resource + // s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) } } } diff --git a/server/application/application_test.go b/server/application/application_test.go index a7d30fa92fa17..f3cdedf79047f 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -22,9 +22,12 @@ import ( v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" kubetesting "k8s.io/client-go/testing" k8scache "k8s.io/client-go/tools/cache" "k8s.io/utils/pointer" @@ -78,14 +81,14 @@ func fakeAppList() *apiclient.AppList { } } -func fakeResolveRevesionResponse() *apiclient.ResolveRevisionResponse { +func fakeResolveRevisionResponse() *apiclient.ResolveRevisionResponse { return &apiclient.ResolveRevisionResponse{ Revision: "f9ba9e98119bf8c1176fbd65dbae26a71d044add", AmbiguousRevision: "HEAD (f9ba9e98119bf8c1176fbd65dbae26a71d044add)", } } -func fakeResolveRevesionResponseHelm() *apiclient.ResolveRevisionResponse { +func fakeResolveRevisionResponseHelm() *apiclient.ResolveRevisionResponse { return &apiclient.ResolveRevisionResponse{ Revision: "0.7.*", AmbiguousRevision: "0.7.* (0.7.2)", @@ -100,9 +103,9 @@ func fakeRepoServerClient(isHelm bool) *mocks.RepoServerServiceClient { mockRepoServiceClient.On("TestRepository", mock.Anything, mock.Anything).Return(&apiclient.TestRepositoryResponse{}, nil) if isHelm { - mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevesionResponseHelm(), nil) + mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponseHelm(), nil) } else { - mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevesionResponse(), nil) + mockRepoServiceClient.On("ResolveRevision", mock.Anything, mock.Anything).Return(fakeResolveRevisionResponse(), nil) } return &mockRepoServiceClient @@ -114,10 +117,18 @@ func newTestAppServer(objects ...runtime.Object) *Server { _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) enf.SetDefaultRole("role:admin") } - return newTestAppServerWithEnforcerConfigure(f, objects...) + return newTestAppServerWithEnforcerConfigure(nil, f, objects...) } -func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), objects ...runtime.Object) *Server { +func newTestAppServerWithResourceFunc(getResourceFunc *func(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error), objects ...runtime.Object) *Server { + f := func(enf *rbac.Enforcer) { + _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) + enf.SetDefaultRole("role:admin") + } + return newTestAppServerWithEnforcerConfigure(getResourceFunc, f, objects...) +} + +func newTestAppServerWithEnforcerConfigure(getResourceFunc *func(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error), f func(*rbac.Enforcer), objects ...runtime.Object) *Server { kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: testNamespace, @@ -202,6 +213,10 @@ func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), objects ...ru panic("Timed out waiting for caches to sync") } + mockKubectlCmd := &kubetest.MockKubectlCmd{} + if getResourceFunc != nil { + mockKubectlCmd.WithGetResourceFunc(*getResourceFunc) + } server, _ := NewServer( testNamespace, kubeclientset, @@ -210,7 +225,7 @@ func newTestAppServerWithEnforcerConfigure(f func(*rbac.Enforcer), objects ...ru appInformer, mockRepoClient, nil, - &kubetest.MockKubectlCmd{}, + mockKubectlCmd, db, enforcer, sync.NewKeyLock(), @@ -450,7 +465,7 @@ g, group-49, role:test3 ` _ = enf.SetUserPolicy(policy) } - appServer := newTestAppServerWithEnforcerConfigure(f, objects...) + appServer := newTestAppServerWithEnforcerConfigure(nil, f, objects...) res, err := appServer.List(ctx, &application.ApplicationQuery{}) @@ -1146,39 +1161,85 @@ func TestInferResourcesStatusHealth(t *testing.T) { assert.Nil(t, testApp.Status.Resources[1].Health) } -func TestRunResourceAction_WithCreateOperationPermitted(t *testing.T) { - testApp := newTestApp() - appServer := newTestAppServer(testApp) +func returnCronJob() *unstructured.Unstructured { + return &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "my-cron-job", + "namespace": testNamespace, + }, + "spec": map[string]interface{}{ + "jobTemplate": map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{}, + }, + }, + }, + }, + }} +} + +func TestRunResourceAction(t *testing.T) { + cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) group := "batch" kind := "CronJob" version := "v1" resourceName := "my-cron-job" - namespace := "kuku" + namespace := testNamespace action := "create-from" + uid := "1" + + resources := []appsv1.ResourceStatus{{ + Group: group, + Kind: kind, + Name: resourceName, + Namespace: testNamespace, + Version: version, + }} - _, err := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ - Name: &testApp.Name, - Namespace: &namespace, - Action: &action, - AppNamespace: &testApp.Namespace, - ResourceName: &resourceName, - Version: &version, - Group: &group, - Kind: &kind, - }) + getResourceFunc := func(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { + return returnCronJob(), nil + } - assert.NoError(t, err) -} + appStateCache := appstate.NewCache(cacheClient, time.Minute) -func TestRunResourceAction_WithCreateOperationDenied(t *testing.T) { + nodes := []appsv1.ResourceNode{{ + ResourceRef: appsv1.ResourceRef{ + Group: group, + Kind: kind, + Version: version, + Name: resourceName, + Namespace: testNamespace, + UID: uid, + }, + }} -} + t.Run("CreateOperationNotPermitted", func(t *testing.T) { + testApp := newTestApp() + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources -func TestRunResourceAction_WithUpdateOperationPermitted(t *testing.T) { + appServer := newTestAppServerWithResourceFunc(&getResourceFunc, testApp) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) -} + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) -func TestRunResourceAction_WithUpdateOperationDenied(t *testing.T) { + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + assert.Contains(t, runErr.Error(), "creation not permitted in project") + assert.Nil(t, appResponse) + }) } diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 1cca927dc3690..10d4447a23061 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -92,8 +92,11 @@ func TestLuaResourceActionsScript(t *testing.T) { if impactedResource.K8SOperation == "patch" { // Patching is only allowed for the source resource, so the result must have exactly 1 resource assert.EqualValues(t, 1, len(results)) - // Patching is only allowed for the source resource, so the kind must be the same - assert.EqualValues(t, obj.GetKind(), impactedResource.UnstructuredObj.GetKind()) + // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource + assert.EqualValues(t, obj.GetKind(), result.GetKind()) + assert.EqualValues(t, obj.GetAPIVersion(), result.GetAPIVersion()) + assert.EqualValues(t, obj.GetName(), result.GetName()) + assert.EqualValues(t, obj.GetNamespace(), result.GetNamespace()) expectedObj := getObj(filepath.Join(dir, test.ExpectedOutputPath)) // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. @@ -106,7 +109,8 @@ func TestLuaResourceActionsScript(t *testing.T) { } } else { if impactedResource.K8SOperation == "create" { - t.Error("There is a create action - hooray! But I don't know ho to test it yet") + t.Log("There is a create action - hooray! But I don't know how to test it yet") + t.Log(result.GetKind(), result.GetName(), result.GetNamespace()) } } diff --git a/util/lua/lua.go b/util/lua/lua.go index 8fb00dd0cc569..afff421f9803d 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -179,7 +179,7 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string // The logic, that goes over the array returned from Lua, and makes sure that: // - there is at most 1 patch operation // - if such patch operation exists, it is being invoked only on the source resource - // will be somewhere here, and if one those is violated, an error will ne thrown + // will be somewhere here, and if one those is violated, an error will be thrown impactedResources := make([]ImpactedResource, 0) if newObj.GetKind() == "Job" { impactedResources = append(impactedResources, ImpactedResource{newObj, "create"}) From 84f0b33fef1a3f0e8d1f8afc15f7cdb673928aef Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 24 Feb 2023 13:02:05 +0200 Subject: [PATCH 13/38] added a ns in the action.lua and fixed tests Signed-off-by: reggie --- .../CronJob/actions/create-from/action.lua | 1 + .../CronJob/actions/testdata/cronjob.yaml | 1 + .../batch/CronJob/actions/testdata/job.yaml | 1 + server/application/application.go | 1 + server/application/application_test.go | 39 ++++++++++++++++++- util/lua/custom_actions_test.go | 32 ++++++++++++++- util/lua/lua_test.go | 13 ++++--- 7 files changed, 80 insertions(+), 8 deletions(-) diff --git a/resource_customizations/batch/CronJob/actions/create-from/action.lua b/resource_customizations/batch/CronJob/actions/create-from/action.lua index b317d8a01f824..cbe5595ab0cfa 100644 --- a/resource_customizations/batch/CronJob/actions/create-from/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-from/action.lua @@ -4,6 +4,7 @@ job.apiVersion = "batch/v1" job.kind = "Job" job.metadata = {} job.metadata.name = obj.metadata.name .. os.date("!%Y%m%d%H%M") +job.metadata.namespace = obj.metadata.namespace job.spec = {} job.spec.template = {} job.spec.template.spec = obj.spec.jobTemplate.spec.template.spec diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml index 3ab1fb9b1cd8a..e7fde2c3b1430 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml @@ -2,6 +2,7 @@ apiVersion: batch/v1 kind: CronJob metadata: name: hello + namespace: test-ns spec: schedule: "* * * * *" jobTemplate: diff --git a/resource_customizations/batch/CronJob/actions/testdata/job.yaml b/resource_customizations/batch/CronJob/actions/testdata/job.yaml index bc7235b297b36..83570da514dfe 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/job.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/job.yaml @@ -2,6 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: name: hello-123 + namespace: test-ns spec: template: spec: diff --git a/server/application/application.go b/server/application/application.go index e5b8e345a7804..64bb97f85c24a 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2137,6 +2137,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA if !permitted { return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) } + // Create the resource // s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) } diff --git a/server/application/application_test.go b/server/application/application_test.go index f3cdedf79047f..cc95bfa2b7f9e 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -170,6 +170,7 @@ func newTestAppServerWithEnforcerConfigure(getResourceFunc *func(ctx context.Con Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, }, } + projWithSyncWindows := &appsv1.AppProject{ ObjectMeta: metav1.ObjectMeta{Name: "proj-maint", Namespace: "default"}, Spec: appsv1.AppProjectSpec{ @@ -1217,12 +1218,22 @@ func TestRunResourceAction(t *testing.T) { }, }} + createJobDenyingProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "createJobDenyingProj", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "kuku", Kind: "muku"}}, + }, + } + t.Run("CreateOperationNotPermitted", func(t *testing.T) { testApp := newTestApp() + testApp.Spec.Project = "createJobDenyingProj" testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree testApp.Status.Resources = resources - appServer := newTestAppServerWithResourceFunc(&getResourceFunc, testApp) + appServer := newTestAppServerWithResourceFunc(&getResourceFunc, testApp, createJobDenyingProj) appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) @@ -1242,4 +1253,30 @@ func TestRunResourceAction(t *testing.T) { assert.Contains(t, runErr.Error(), "creation not permitted in project") assert.Nil(t, appResponse) }) + + t.Run("CreateOperationPermitted", func(t *testing.T) { + testApp := newTestApp() + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources + + appServer := newTestAppServerWithResourceFunc(&getResourceFunc, testApp) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) + + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) + + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + require.NoError(t, runErr) + assert.NotNil(t, appResponse) + }) } diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 10d4447a23061..b685934052aa6 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -19,6 +19,36 @@ import ( type testNormalizer struct{} func (t testNormalizer) Normalize(un *unstructured.Unstructured) error { + if un == nil { + return nil + } + switch un.GetKind() { + case "DaemonSet", "Deployment", "StatefulSet": + err := unstructured.SetNestedStringMap(un.Object, map[string]string{"kubectl.kubernetes.io/restartedAt": "0001-01-01T00:00:00Z"}, "spec", "template", "metadata", "annotations") + if err != nil { + return fmt.Errorf("failed to normalize DaemonSet: %w", err) + } + } + switch un.GetKind() { + case "Deployment": + err := unstructured.SetNestedField(un.Object, nil, "status") + if err != nil { + return fmt.Errorf("failed to normalize DaemonSet: %w", err) + } + err = unstructured.SetNestedField(un.Object, nil, "metadata", "creationTimestamp") + if err != nil { + return fmt.Errorf("failed to normalize DaemonSet: %w", err) + } + err = unstructured.SetNestedField(un.Object, nil, "metadata", "generation") + if err != nil { + return fmt.Errorf("failed to normalize DaemonSet: %w", err) + } + case "Rollout": + err := unstructured.SetNestedField(un.Object, nil, "spec", "restartAt") + if err != nil { + return fmt.Errorf("failed to normalize Rollout: %w", err) + } + } return nil } @@ -90,8 +120,6 @@ func TestLuaResourceActionsScript(t *testing.T) { for _, impactedResource := range results { result := impactedResource.UnstructuredObj if impactedResource.K8SOperation == "patch" { - // Patching is only allowed for the source resource, so the result must have exactly 1 resource - assert.EqualValues(t, 1, len(results)) // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource assert.EqualValues(t, obj.GetKind(), result.GetKind()) assert.EqualValues(t, obj.GetAPIVersion(), result.GetAPIVersion()) diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index 314f9a8ab313c..358e6cb315212 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -334,9 +334,11 @@ func TestExecuteResourceAction(t *testing.T) { testObj := StrToUnstructured(objJSON) expectedObj := StrToUnstructured(expectedUpdatedObj) vm := VM{} - newObj, err := vm.ExecuteResourceAction(testObj, validActionLua) + newObjects, err := vm.ExecuteResourceAction(testObj, validActionLua) assert.Nil(t, err) - assert.Equal(t, expectedObj, newObj) + assert.Equal(t, len(newObjects), 1) + assert.Equal(t, newObjects[0].K8SOperation, "patch") + assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj) } func TestExecuteResourceActionNonTableReturn(t *testing.T) { @@ -409,10 +411,11 @@ func TestCleanPatch(t *testing.T) { testObj := StrToUnstructured(objWithEmptyStruct) expectedObj := StrToUnstructured(expectedUpdatedObjWithEmptyStruct) vm := VM{} - newObj, err := vm.ExecuteResourceAction(testObj, pausedToFalseLua) + newObjects, err := vm.ExecuteResourceAction(testObj, pausedToFalseLua) assert.Nil(t, err) - assert.Equal(t, expectedObj, newObj) - + assert.Equal(t, len(newObjects), 1) + assert.Equal(t, newObjects[0].K8SOperation, "patch") + assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj) } func TestGetResourceHealth(t *testing.T) { From e6c078834d69a3838c286789439633fcb415cfe7 Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 3 Mar 2023 13:00:58 +0200 Subject: [PATCH 14/38] create-job Signed-off-by: reggie --- go.mod | 2 ++ go.sum | 4 ++-- .../batch/CronJob/actions/action_test.yaml | 2 +- .../CronJob/actions/{create-from => create-job}/action.lua | 0 resource_customizations/batch/CronJob/actions/discovery.lua | 2 +- server/application/application.go | 2 +- server/application/application_test.go | 2 +- util/lua/lua.go | 3 +-- 8 files changed, 9 insertions(+), 8 deletions(-) rename resource_customizations/batch/CronJob/actions/{create-from => create-job}/action.lua (100%) diff --git a/go.mod b/go.mod index 2db7a5075df25..272eb244df023 100644 --- a/go.mod +++ b/go.mod @@ -299,3 +299,5 @@ replace ( k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) + +replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994 diff --git a/go.sum b/go.sum index 0d7ba73f6e327..1972b0c7bca99 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,6 @@ github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd h1:4Y76oXOZ2b7px7ppRSNpdxFPhUEw5e3BYEWpxn8pO2I= -github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.3.1-0.20221203221941-490d98afd1d6 h1:b92Xft7MQv/SP56FW08zt5CMTE1rySH8UPDKOAgSzOM= github.com/argoproj/notifications-engine v0.3.1-0.20221203221941-490d98afd1d6/go.mod h1:pgPU59KCsBOMhyw9amRWPoSuBmUWvx3Xsc5r0mUriLg= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= @@ -983,6 +981,8 @@ github.com/quobyte/api v0.1.8/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H github.com/r3labs/diff v1.1.0 h1:V53xhrbTHrWFWq3gI4b94AjgEJOerO1+1l0xyHOBi8M= github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6Xig= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994 h1:/IlwxHOeQYZT0SwBVNVTVXksY3wiKzppEXs5WvjRxXo= +github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/resource_customizations/batch/CronJob/actions/action_test.yaml b/resource_customizations/batch/CronJob/actions/action_test.yaml index cea31e7aabc15..a9b5320db5721 100644 --- a/resource_customizations/batch/CronJob/actions/action_test.yaml +++ b/resource_customizations/batch/CronJob/actions/action_test.yaml @@ -1,4 +1,4 @@ actionTests: -- action: create-from +- action: create-job inputPath: testdata/cronjob.yaml expectedOutputPath: testdata/job.yaml diff --git a/resource_customizations/batch/CronJob/actions/create-from/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua similarity index 100% rename from resource_customizations/batch/CronJob/actions/create-from/action.lua rename to resource_customizations/batch/CronJob/actions/create-job/action.lua diff --git a/resource_customizations/batch/CronJob/actions/discovery.lua b/resource_customizations/batch/CronJob/actions/discovery.lua index a44f35a606f6f..f90293c1aa671 100644 --- a/resource_customizations/batch/CronJob/actions/discovery.lua +++ b/resource_customizations/batch/CronJob/actions/discovery.lua @@ -1,3 +1,3 @@ actions = {} -actions["create-from"] = {} +actions["create-job"] = {} return actions \ No newline at end of file diff --git a/server/application/application.go b/server/application/application.go index 64bb97f85c24a..12dce5231a567 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2139,7 +2139,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } // Create the resource - // s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) } } } diff --git a/server/application/application_test.go b/server/application/application_test.go index cc95bfa2b7f9e..ea9af214ae8dc 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1190,7 +1190,7 @@ func TestRunResourceAction(t *testing.T) { version := "v1" resourceName := "my-cron-job" namespace := testNamespace - action := "create-from" + action := "create-job" uid := "1" resources := []appsv1.ResourceStatus{{ diff --git a/util/lua/lua.go b/util/lua/lua.go index afff421f9803d..cf452dd0779c5 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -177,8 +177,7 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string // Meanwhile, just wrapping Job resource with a "create" operation, and wrapping everything else with a "patch" operation. // The "everything else" are traditional outputs from a ResourceAction - the update to the same resource the action is invoked on. // The logic, that goes over the array returned from Lua, and makes sure that: - // - there is at most 1 patch operation - // - if such patch operation exists, it is being invoked only on the source resource + // - if a patch operation exists, it is being invoked only on the source resource // will be somewhere here, and if one those is violated, an error will be thrown impactedResources := make([]ImpactedResource, 0) if newObj.GetKind() == "Job" { From 512068dd7bb60c115aa2bcc00d1e843e8621fa19 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 4 Mar 2023 18:11:42 +0200 Subject: [PATCH 15/38] in progress Signed-off-by: reggie --- .../batch/CronJob/actions/create-job/action.lua | 13 ++++++++++--- server/application/application.go | 5 ++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index cbe5595ab0cfa..fdd89d459415f 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -1,18 +1,25 @@ local os = require("os") + job = {} job.apiVersion = "batch/v1" job.kind = "Job" job.metadata = {} + job.metadata.name = obj.metadata.name .. os.date("!%Y%m%d%H%M") job.metadata.namespace = obj.metadata.namespace + job.spec = {} job.spec.template = {} -job.spec.template.spec = obj.spec.jobTemplate.spec.template.spec -job.metadata.ownerReferences = {} +job.spec.template.spec = {} +job.spec.template.spec.containers = obj.spec.jobTemplate.spec.template.spec.containers +job.spec.template.spec.restartPolicy = obj.spec.jobTemplate.spec.template.spec.restartPolicy +job.spec.suspend = false + ownerRef = {} ownerRef.apiVersion = obj.apiVersion ownerRef.kind = "CronJob" ownerRef.name = obj.metadata.name ownerRef.uid = obj.metadata.uid --- job.metadata.ownerReferences[0] = ownerRef +job.metadata.ownerReferences = {} +job.metadata.ownerReferences[0] = ownerRef return job \ No newline at end of file diff --git a/server/application/application.go b/server/application/application.go index 12dce5231a567..dccfdb4c6b746 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2139,7 +2139,10 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } // Create the resource - s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + _, err = s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + if err != nil { + return nil, fmt.Errorf("error creating resource: %w", err) + } } } } From ea71a0e08ad535c6093d82f46b368b47cfeae2b6 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 11 Mar 2023 23:00:20 +0200 Subject: [PATCH 16/38] more changes Signed-off-by: reggie --- go.mod | 2 +- go.sum | 8 ++-- .../CronJob/actions/create-job/action.lua | 43 +++++++++++++------ .../CronJob/actions/testdata/cronjob.yaml | 1 + server/application/application.go | 10 +++++ server/application/application_test.go | 14 +++++- util/lua/custom_actions_test.go | 1 + util/lua/health_test.go | 3 ++ util/lua/lua.go | 3 ++ 9 files changed, 66 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index eb33f60b5fb7e..3b0ad74d33c71 100644 --- a/go.mod +++ b/go.mod @@ -298,4 +298,4 @@ replace ( k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) -replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994 +replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8 diff --git a/go.sum b/go.sum index 4f658206af756..482b6287102c2 100644 --- a/go.sum +++ b/go.sum @@ -142,8 +142,8 @@ github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/notifications-engine v0.3.1-0.20221203221941-490d98afd1d6 h1:b92Xft7MQv/SP56FW08zt5CMTE1rySH8UPDKOAgSzOM= -github.com/argoproj/notifications-engine v0.3.1-0.20221203221941-490d98afd1d6/go.mod h1:pgPU59KCsBOMhyw9amRWPoSuBmUWvx3Xsc5r0mUriLg= +github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da h1:Vf9xvHcXn4TP/nLIfWn+TaC521V9fpz/DwRP6uEeVR8= +github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da/go.mod h1:05koR0gE/O0i5YDbidg1dpr76XitK4DJveh+dIAq6e8= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d/go.mod h1:RKjj5FJ6KxtktOY49GJSG49qO6Z4lH7RnrVCaS3tf18= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -976,8 +976,8 @@ github.com/quobyte/api v0.1.8/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H github.com/r3labs/diff v1.1.0 h1:V53xhrbTHrWFWq3gI4b94AjgEJOerO1+1l0xyHOBi8M= github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6Xig= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994 h1:/IlwxHOeQYZT0SwBVNVTVXksY3wiKzppEXs5WvjRxXo= -github.com/reggie-k/gitops-engine v0.0.0-20230302155252-99968ef41994/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8 h1:N/xjODApWYsYDtQc9sE5sUlQtlV6Fr+tqsfe2a9dyMI= +github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index fdd89d459415f..0d76c97feffd7 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -1,25 +1,42 @@ local os = require("os") +function deepCopy(obj, seen) + -- Handle non-tables and previously-seen tables. + if type(obj) ~= 'table' then + return obj end + if seen and seen[obj] then return seen[obj] end + + -- New table; mark it as seen and copy recursively. + local s = seen or {} + local res = {} + s[obj] = res + for k, v in pairs(obj) do res[deepCopy(k, s)] = deepCopy(v, s) end + return setmetatable(res, getmetatable(obj)) +end + job = {} job.apiVersion = "batch/v1" job.kind = "Job" -job.metadata = {} -job.metadata.name = obj.metadata.name .. os.date("!%Y%m%d%H%M") +job.metadata = {} +job.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") job.metadata.namespace = obj.metadata.namespace - -job.spec = {} -job.spec.template = {} -job.spec.template.spec = {} -job.spec.template.spec.containers = obj.spec.jobTemplate.spec.template.spec.containers -job.spec.template.spec.restartPolicy = obj.spec.jobTemplate.spec.template.spec.restartPolicy -job.spec.suspend = false +job.metadata.ownerReferences = {} ownerRef = {} ownerRef.apiVersion = obj.apiVersion -ownerRef.kind = "CronJob" +ownerRef.kind = obj.kind ownerRef.name = obj.metadata.name ownerRef.uid = obj.metadata.uid -job.metadata.ownerReferences = {} -job.metadata.ownerReferences[0] = ownerRef -return job \ No newline at end of file + +job.metadata.ownerReferences[1] = ownerRef + +job.spec = {} +job.spec.suspend = false +job.spec.template = {} + +job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) +print ("deep copied") +print (job.spec.template.spec) +return job + diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml index e7fde2c3b1430..58f8730ecdff8 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml @@ -3,6 +3,7 @@ kind: CronJob metadata: name: hello namespace: test-ns + uid: "123" spec: schedule: "* * * * *" jobTemplate: diff --git a/server/application/application.go b/server/application/application.go index dccfdb4c6b746..ac5b31e57ae9f 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -1,6 +1,7 @@ package application import ( + "bytes" "context" "encoding/json" "errors" @@ -1989,6 +1990,9 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque if err != nil { return nil, nil, nil, nil, fmt.Errorf("error getting application cluster config: %w", err) } + data, _ := json.Marshal(obj) + fmt.Print("************************************ app " + bytes.NewBuffer(data).String() + "*************************************") + obj, err = kube.ToUnstructured(app) } else { res, config, app, err = s.getAppLiveResource(ctx, rbacRequest, q) @@ -1996,6 +2000,9 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque return nil, nil, nil, nil, fmt.Errorf("error getting app live resource: %w", err) } obj, err = s.kubectl.GetResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace) + data, _ := json.Marshal(obj) + fmt.Print("************************************ GetResource " + bytes.NewBuffer(data).String() + "*************************************") + } if err != nil { return nil, nil, nil, nil, fmt.Errorf("error getting resource: %w", err) @@ -2143,6 +2150,9 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA if err != nil { return nil, fmt.Errorf("error creating resource: %w", err) } + + } else { + return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) } } } diff --git a/server/application/application_test.go b/server/application/application_test.go index ea9af214ae8dc..186366b5234a1 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1172,12 +1172,24 @@ func returnCronJob() *unstructured.Unstructured { }, "spec": map[string]interface{}{ "jobTemplate": map[string]interface{}{ + "ku0": "ku0", "spec": map[string]interface{}{ + "ku1": "ku1", "template": map[string]interface{}{ - "spec": map[string]interface{}{}, + "ku2": "ku2", + "spec": map[string]interface{}{ + "ku3": "ku3", + "containers": []map[string]interface{}{{ + "name": "hello", + "image": "busybox:1.28", + "imagePullPolicy": "IfNotPresent", + "command": []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}}}, + "restartPolicy": "OnFailure", + }, }, }, }, + "schedule": "* * * * *", }, }} } diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index b685934052aa6..84987caa1daf1 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -139,6 +139,7 @@ func TestLuaResourceActionsScript(t *testing.T) { if impactedResource.K8SOperation == "create" { t.Log("There is a create action - hooray! But I don't know how to test it yet") t.Log(result.GetKind(), result.GetName(), result.GetNamespace()) + } } diff --git a/util/lua/health_test.go b/util/lua/health_test.go index 1052f5c8fa95a..dad4696388817 100644 --- a/util/lua/health_test.go +++ b/util/lua/health_test.go @@ -1,6 +1,8 @@ package lua import ( + "bytes" + "fmt" "os" "path/filepath" "strings" @@ -25,6 +27,7 @@ type IndividualTest struct { func getObj(path string) *unstructured.Unstructured { yamlBytes, err := os.ReadFile(path) + fmt.Print("************************************ input " + bytes.NewBuffer(yamlBytes).String() + "*************************************") errors.CheckError(err) obj := make(map[string]interface{}) err = yaml.Unmarshal(yamlBytes, &obj) diff --git a/util/lua/lua.go b/util/lua/lua.go index cf452dd0779c5..64c04deb0cacc 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -1,6 +1,7 @@ package lua import ( + "bytes" "context" "encoding/json" "fmt" @@ -163,6 +164,8 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string returnValue := l.Get(-1) if returnValue.Type() == lua.LTTable { jsonBytes, err := luajson.Encode(returnValue) + + fmt.Print("************************************" + bytes.NewBuffer(jsonBytes).String() + "*************************************") if err != nil { return nil, err } From b28298e2b7f9dcd1518d10d848c627296b25fac7 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 18 Mar 2023 20:11:50 +0200 Subject: [PATCH 17/38] full unit tests and action returning an array Signed-off-by: reggie --- go.mod | 2 +- go.sum | 4 +- pkg/apis/application/v1alpha1/types.go | 13 -- .../CronJob/actions/create-job/action.lua | 52 +++--- .../CronJob/actions/testdata/cronjob.yaml | 1 + server/application/application.go | 151 +++++++++--------- server/application/application_test.go | 1 + util/lua/lua.go | 76 ++++++--- 8 files changed, 172 insertions(+), 128 deletions(-) diff --git a/go.mod b/go.mod index 3b0ad74d33c71..33bf735edb635 100644 --- a/go.mod +++ b/go.mod @@ -298,4 +298,4 @@ replace ( k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) -replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8 +replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 diff --git a/go.sum b/go.sum index 482b6287102c2..3dd9e5472ac6f 100644 --- a/go.sum +++ b/go.sum @@ -976,8 +976,8 @@ github.com/quobyte/api v0.1.8/go.mod h1:jL7lIHrmqQ7yh05OJ+eEEdHr0u/kmT1Ff9iHd+4H github.com/r3labs/diff v1.1.0 h1:V53xhrbTHrWFWq3gI4b94AjgEJOerO1+1l0xyHOBi8M= github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6Xig= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8 h1:N/xjODApWYsYDtQc9sE5sUlQtlV6Fr+tqsfe2a9dyMI= -github.com/reggie-k/gitops-engine v0.0.0-20230310173323-164da0b6d2e8/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 h1:47IKb6USJNlvb0Gf/m1cOTA0UCnaGOnQh91TI+ylQJk= +github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index 8282361d4e00e..442f786dbe103 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -2663,19 +2663,6 @@ func UnmarshalToUnstructured(resource string) (*unstructured.Unstructured, error return &obj, nil } -// UnmarshalToUnstructuredList unmarshals a resource list representation in JSON to unstructured data list -func UnmarshalToUnstructuredList(listResource string) (*unstructured.UnstructuredList, error) { - if listResource == "" || listResource == "null" { - return nil, nil - } - var obj unstructured.UnstructuredList - err := json.Unmarshal([]byte(listResource), &obj) - if err != nil { - return nil, err - } - return &obj, nil -} - // TODO: document this method func (r ResourceDiff) LiveObject() (*unstructured.Unstructured, error) { return UnmarshalToUnstructured(r.LiveState) diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index 0d76c97feffd7..65eb0715d87d4 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -1,17 +1,31 @@ local os = require("os") -function deepCopy(obj, seen) - -- Handle non-tables and previously-seen tables. - if type(obj) ~= 'table' then - return obj end - if seen and seen[obj] then return seen[obj] end - - -- New table; mark it as seen and copy recursively. - local s = seen or {} - local res = {} - s[obj] = res - for k, v in pairs(obj) do res[deepCopy(k, s)] = deepCopy(v, s) end - return setmetatable(res, getmetatable(obj)) +-- This action constructs a Job resource from a CronJob resource, to enable creating a CronJob instance on demand. +-- It returns an array with a single member - a table with the operation to perform (create) and the Job resource. +-- It mimics the output of "kubectl create job --from=" command, declaratively. + +-- Deep-copying an object is a ChatGPT generated code. +-- Since empty tables are treated as empty arrays, the resulting k8s resource might be invalid (arrays instead of maps). +-- So empty tables are not cloned to the target object. +function deepCopy(object) + local lookup_table = {} + local function _copy(obj) + if type(obj) ~= "table" then + return obj + elseif lookup_table[obj] then + return lookup_table[obj] + elseif next(obj) == nil then + return nil + else + local new_table = {} + lookup_table[obj] = new_table + for key, value in pairs(obj) do + new_table[_copy(key)] = _copy(value) + end + return setmetatable(new_table, getmetatable(obj)) + end + end + return _copy(object) end job = {} @@ -21,22 +35,24 @@ job.kind = "Job" job.metadata = {} job.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") job.metadata.namespace = obj.metadata.namespace -job.metadata.ownerReferences = {} ownerRef = {} ownerRef.apiVersion = obj.apiVersion ownerRef.kind = obj.kind ownerRef.name = obj.metadata.name ownerRef.uid = obj.metadata.uid - +job.metadata.ownerReferences = {} job.metadata.ownerReferences[1] = ownerRef job.spec = {} job.spec.suspend = false job.spec.template = {} - job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) -print ("deep copied") -print (job.spec.template.spec) -return job +impactedResource = {} +impactedResource.K8SOperation = "create" +impactedResource.UnstructuredObj = job +result = {} +result[1] = impactedResource + +return result \ No newline at end of file diff --git a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml index 58f8730ecdff8..118fc83929e96 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/cronjob.yaml @@ -18,4 +18,5 @@ spec: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster + resources: {} restartPolicy: OnFailure \ No newline at end of file diff --git a/server/application/application.go b/server/application/application.go index ac5b31e57ae9f..9bab7ae9329b6 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2082,78 +2082,13 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error marshaling live object: %w", err) } - if impactedResource.K8SOperation == "patch" { - - diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) - if err != nil { - return nil, fmt.Errorf("error calculating merge patch: %w", err) - } - if string(diffBytes) == "{}" { - return &application.ApplicationResponse{}, nil - } - - // The following logic detects if the resource action makes a modification to status and/or spec. - // If status was modified, we attempt to patch the status using status subresource, in case the - // CRD is configured using the status subresource feature. See: - // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource - // If status subresource is in use, the patch has to be split into two: - // * one to update spec (and other non-status fields) - // * the other to update only status. - nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) - if err != nil { - return nil, fmt.Errorf("error splitting status patch: %w", err) - } - if statusPatch != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") - if err != nil { - if !apierr.IsNotFound(err) { - return nil, fmt.Errorf("error patching resource: %w", err) - } - // K8s API server returns 404 NotFound when the CRD does not support the status subresource - // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch - } else { - // If we get here, the CRD does use the status subresource, so we must patch status and - // spec separately. update the diffBytes to the spec-only patch and fall through. - diffBytes = nonStatusPatch - } - } - if diffBytes != nil { - _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) - if err != nil { - return nil, fmt.Errorf("error patching resource: %w", err) - } - } - } else { - if impactedResource.K8SOperation == "create" { - proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) - if err != nil { - if apierr.IsNotFound(err) { - return nil, fmt.Errorf("application references project %s which does not exist", app.Spec.Project) - } - } - permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: newObj.GroupVersionKind().Group, Kind: newObj.GroupVersionKind().Kind}, newObj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { - clusters, err := s.db.GetProjectClusters(context.TODO(), project) - if err != nil { - return nil, fmt.Errorf("failed to get project clusters: %w", err) - } - return clusters, nil - }) - if err != nil { - return nil, fmt.Errorf("error checking resource permissions: %w", err) - } - if !permitted { - return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) - } - - // Create the resource - _, err = s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) - if err != nil { - return nil, fmt.Errorf("error creating resource: %w", err) - } - - } else { - return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) - } + switch impactedResource.K8SOperation { + case "patch": + return s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) + case "create": + return s.createResource(ctx, config, app, newObj) + default: + return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) } } @@ -2166,6 +2101,78 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return &application.ApplicationResponse{}, nil } +func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObjBytes, newObjBytes []byte, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { + diffBytes, err := jsonpatch.CreateMergePatch(liveObjBytes, newObjBytes) + if err != nil { + return nil, fmt.Errorf("error calculating merge patch: %w", err) + } + if string(diffBytes) == "{}" { + return &application.ApplicationResponse{}, nil + } + + // The following logic detects if the resource action makes a modification to status and/or spec. + // If status was modified, we attempt to patch the status using status subresource, in case the + // CRD is configured using the status subresource feature. See: + // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#status-subresource + // If status subresource is in use, the patch has to be split into two: + // * one to update spec (and other non-status fields) + // * the other to update only status. + nonStatusPatch, statusPatch, err := splitStatusPatch(diffBytes) + if err != nil { + return nil, fmt.Errorf("error splitting status patch: %w", err) + } + if statusPatch != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes, "status") + if err != nil { + if !apierr.IsNotFound(err) { + return nil, fmt.Errorf("error patching resource: %w", err) + } + // K8s API server returns 404 NotFound when the CRD does not support the status subresource + // if we get here, the CRD does not use the status subresource. We will fall back to a normal patch + } else { + // If we get here, the CRD does use the status subresource, so we must patch status and + // spec separately. update the diffBytes to the spec-only patch and fall through. + diffBytes = nonStatusPatch + } + } + if diffBytes != nil { + _, err = s.kubectl.PatchResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), types.MergePatchType, diffBytes) + if err != nil { + return nil, fmt.Errorf("error patching resource: %w", err) + } + } + return &application.ApplicationResponse{}, nil +} + +func (s *Server) createResource(ctx context.Context, config *rest.Config, app *appv1.Application, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { + proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) + if err != nil { + if apierr.IsNotFound(err) { + return nil, fmt.Errorf("application references project %s which does not exist", app.Spec.Project) + } + } + permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: newObj.GroupVersionKind().Group, Kind: newObj.GroupVersionKind().Kind}, newObj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { + clusters, err := s.db.GetProjectClusters(context.TODO(), project) + if err != nil { + return nil, fmt.Errorf("failed to get project clusters: %w", err) + } + return clusters, nil + }) + if err != nil { + return nil, fmt.Errorf("error checking resource permissions: %w", err) + } + if !permitted { + return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) + } + + // Create the resource + _, err = s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + if err != nil { + return nil, fmt.Errorf("error creating resource: %w", err) + } + return &application.ApplicationResponse{}, nil +} + // splitStatusPatch splits a patch into two: one for a non-status patch, and the status-only patch. // Returns nil for either if the patch doesn't have modifications to non-status, or status, respectively. func splitStatusPatch(patch []byte) ([]byte, []byte, error) { diff --git a/server/application/application_test.go b/server/application/application_test.go index 186366b5234a1..50cbf93775b5e 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1184,6 +1184,7 @@ func returnCronJob() *unstructured.Unstructured { "image": "busybox:1.28", "imagePullPolicy": "IfNotPresent", "command": []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}}}, + // "resources": {}, "restartPolicy": "OnFailure", }, }, diff --git a/util/lua/lua.go b/util/lua/lua.go index 64c04deb0cacc..9dfa5ac7bdf19 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -21,11 +21,13 @@ import ( ) const ( - incorrectReturnType = "expect %s output from Lua script, not %s" - invalidHealthStatus = "Lua returned an invalid health status" - healthScriptFile = "health.lua" - actionScriptFile = "action.lua" - actionDiscoveryScriptFile = "discovery.lua" + incorrectReturnType = "expect %s output from Lua script, not %s" + incorrectInnerType = "expect %s inner type from Lua script, not %s" + invalidHealthStatus = "Lua returned an invalid health status" + healthScriptFile = "health.lua" + actionScriptFile = "action.lua" + actionDiscoveryScriptFile = "discovery.lua" + luaImpactedResourceTypeName = "impactedResource" ) type ResourceHealthOverrides map[string]appv1.ResourceOverride @@ -61,6 +63,8 @@ type VM struct { // that will need to be performed on this returned resource. // This replaces the traditional architecture of "Lua action returns a resource that ArgoCD will patch". // This enables ArgoCD to create NEW resources upon custom actions. +// Note that the Lua code in the custom action is coupled to this type, and must return a json output with exactly those fields, +// since the json output is then unmarshalled to this struct. type ImpactedResource struct { UnstructuredObj *unstructured.Unstructured K8SOperation string @@ -110,6 +114,7 @@ func (vm VM) ExecuteHealthLua(obj *unstructured.Unstructured, script string) (*h } returnValue := l.Get(-1) if returnValue.Type() == lua.LTTable { + jsonBytes, err := luajson.Encode(returnValue) if err != nil { return nil, err @@ -165,34 +170,61 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string if returnValue.Type() == lua.LTTable { jsonBytes, err := luajson.Encode(returnValue) - fmt.Print("************************************" + bytes.NewBuffer(jsonBytes).String() + "*************************************") - if err != nil { - return nil, err - } - newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes)) if err != nil { return nil, err } - cleanedNewObj := cleanReturnedObj(newObj.Object, obj.Object) - newObj.Object = cleanedNewObj - - // TODO: delete this hard coded thingie when Lua script returns an array of objects along with corresponding actions. - // Meanwhile, just wrapping Job resource with a "create" operation, and wrapping everything else with a "patch" operation. - // The "everything else" are traditional outputs from a ResourceAction - the update to the same resource the action is invoked on. - // The logic, that goes over the array returned from Lua, and makes sure that: - // - if a patch operation exists, it is being invoked only on the source resource - // will be somewhere here, and if one those is violated, an error will be thrown - impactedResources := make([]ImpactedResource, 0) - if newObj.GetKind() == "Job" { - impactedResources = append(impactedResources, ImpactedResource{newObj, "create"}) + + var impactedResources []ImpactedResource + + jsonString := bytes.NewBuffer(jsonBytes).String() + fmt.Print("************************************" + jsonString + "*************************************") + + // The output from Lua is either an object (old-style action output) or an array (new-style action output). + // Check whether the string starts with an opening square bracket and ends with a closing square bracket, + // avoiding programming by exception. + if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { + // The string represents a new-style action array output + impactedResources, err = unmarshalToImpactedResources(string(jsonBytes)) + if err != nil { + return nil, err + } } else { + // The string represents an old-style action object output + newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes)) + if err != nil { + return nil, err + } + // Wrap the old-style action output with a single-member array. + // The default definition of the old-style action is a "patch" one. impactedResources = append(impactedResources, ImpactedResource{newObj, "patch"}) } + + for _, impactedResource := range impactedResources { + // Cleaning the resource is only relevant to "patch" + if impactedResource.K8SOperation == "patch" { + impactedResource.UnstructuredObj.Object = cleanReturnedObj(impactedResource.UnstructuredObj.Object, obj.Object) + } + + } return impactedResources, nil } return nil, fmt.Errorf(incorrectReturnType, "table", returnValue.Type().String()) } +// UnmarshalToImpactedResources unmarshals an ImpactedResource array representation in JSON to ImpactedResource array +func unmarshalToImpactedResources(resources string) ([]ImpactedResource, error) { + if resources == "" || resources == "null" { + return nil, nil + } + + var impactedResources []ImpactedResource + err := json.Unmarshal([]byte(resources), &impactedResources) + if err != nil { + return nil, err + } + return impactedResources, nil +} + // cleanReturnedObj Lua cannot distinguish an empty table as an array or map, and the library we are using choose to // decoded an empty table into an empty array. This function prevents the lua scripts from unintentionally changing an // empty struct into empty arrays From fa757a9337ceb8a5469b454b969f3c29c91b9b7b Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 18 Mar 2023 20:37:51 +0200 Subject: [PATCH 18/38] cleanup Signed-off-by: reggie --- go.mod | 2 ++ go.sum | 6 +++--- server/application/application_test.go | 4 ---- util/lua/lua.go | 13 ++++++------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 7bc3dc688b021..7d76c2352bfe4 100644 --- a/go.mod +++ b/go.mod @@ -298,3 +298,5 @@ replace ( k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) + +replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 diff --git a/go.sum b/go.sum index 0fd7f7b3ff809..375815f2169e8 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,6 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd h1:4Y76oXOZ2b7px7ppRSNpdxFPhUEw5e3BYEWpxn8pO2I= -github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da h1:Vf9xvHcXn4TP/nLIfWn+TaC521V9fpz/DwRP6uEeVR8= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da/go.mod h1:05koR0gE/O0i5YDbidg1dpr76XitK4DJveh+dIAq6e8= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= @@ -915,6 +913,8 @@ github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= +github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 h1:47IKb6USJNlvb0Gf/m1cOTA0UCnaGOnQh91TI+ylQJk= +github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1882,4 +1882,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/server/application/application_test.go b/server/application/application_test.go index 03bc7b1f3085e..7bede0e656982 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1223,13 +1223,9 @@ func returnCronJob() *unstructured.Unstructured { }, "spec": map[string]interface{}{ "jobTemplate": map[string]interface{}{ - "ku0": "ku0", "spec": map[string]interface{}{ - "ku1": "ku1", "template": map[string]interface{}{ - "ku2": "ku2", "spec": map[string]interface{}{ - "ku3": "ku3", "containers": []map[string]interface{}{{ "name": "hello", "image": "busybox:1.28", diff --git a/util/lua/lua.go b/util/lua/lua.go index 9dfa5ac7bdf19..83aeb852c22af 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -21,13 +21,12 @@ import ( ) const ( - incorrectReturnType = "expect %s output from Lua script, not %s" - incorrectInnerType = "expect %s inner type from Lua script, not %s" - invalidHealthStatus = "Lua returned an invalid health status" - healthScriptFile = "health.lua" - actionScriptFile = "action.lua" - actionDiscoveryScriptFile = "discovery.lua" - luaImpactedResourceTypeName = "impactedResource" + incorrectReturnType = "expect %s output from Lua script, not %s" + incorrectInnerType = "expect %s inner type from Lua script, not %s" + invalidHealthStatus = "Lua returned an invalid health status" + healthScriptFile = "health.lua" + actionScriptFile = "action.lua" + actionDiscoveryScriptFile = "discovery.lua" ) type ResourceHealthOverrides map[string]appv1.ResourceOverride From a35891b50744cd395c78eb4614336778502259ee Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 20 Mar 2023 15:06:11 +0200 Subject: [PATCH 19/38] fix the custom tests Signed-off-by: reggie --- cmd/argocd/commands/admin/settings.go | 13 +- .../actions/testdata/deployment-pause.yaml | 2 + .../actions/testdata/deployment-resume.yaml | 2 + .../CronJob/actions/create-job/action.lua | 4 +- .../batch/CronJob/actions/testdata/job.yaml | 14 +- server/application/application.go | 2 +- util/lua/custom_actions_test.go | 120 ++++++++++++++---- util/lua/health_test.go | 4 +- util/lua/impacted_resource.go | 17 +++ util/lua/lua.go | 13 -- 10 files changed, 134 insertions(+), 57 deletions(-) create mode 100644 util/lua/impacted_resource.go diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index 30026e0d27bdf..6996353f81b7e 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -547,7 +547,8 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a for _, impactedResource := range modifiedRes { result := impactedResource.UnstructuredObj - if impactedResource.K8SOperation == "patch" { + switch impactedResource.K8SOperation { + case "patch": if reflect.DeepEqual(&res, modifiedRes) { _, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name) return @@ -555,13 +556,13 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a _, _ = fmt.Printf("Following fields have been changed:\n\n") _ = cli.PrintDiff(res.GetName(), &res, result) - } else { - if impactedResource.K8SOperation == "create" { - _, _ = fmt.Printf("Create action detected. Don't know what to print yet") - } + case "create": + _, _ = fmt.Printf("Create action detected. Don't know what to print yet") + default: + errors.CheckError(fmt.Errorf("Unsupported operation: %s", impactedResource.K8SOperation)) } - } + }) }, } diff --git a/resource_customizations/apps/Deployment/actions/testdata/deployment-pause.yaml b/resource_customizations/apps/Deployment/actions/testdata/deployment-pause.yaml index 38cb1faf8498f..3ddbbe3e5cef2 100644 --- a/resource_customizations/apps/Deployment/actions/testdata/deployment-pause.yaml +++ b/resource_customizations/apps/Deployment/actions/testdata/deployment-pause.yaml @@ -4,6 +4,8 @@ metadata: annotations: deployment.kubernetes.io/revision: "1" creationTimestamp: "2021-09-21T22:35:20Z" + name: nginx-deploy + namespace: default generation: 2 spec: paused: true diff --git a/resource_customizations/apps/Deployment/actions/testdata/deployment-resume.yaml b/resource_customizations/apps/Deployment/actions/testdata/deployment-resume.yaml index ea8d3b14de51d..8ccb8dcab0802 100644 --- a/resource_customizations/apps/Deployment/actions/testdata/deployment-resume.yaml +++ b/resource_customizations/apps/Deployment/actions/testdata/deployment-resume.yaml @@ -5,6 +5,8 @@ metadata: deployment.kubernetes.io/revision: "1" creationTimestamp: "2021-09-21T22:35:20Z" generation: 3 + name: nginx-deploy + namespace: default spec: progressDeadlineSeconds: 600 replicas: 3 diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index 65eb0715d87d4..6489a66e827a7 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -50,8 +50,8 @@ job.spec.template = {} job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) impactedResource = {} -impactedResource.K8SOperation = "create" -impactedResource.UnstructuredObj = job +impactedResource.k8sOperation = "create" +impactedResource.unstructuredObj = job result = {} result[1] = impactedResource diff --git a/resource_customizations/batch/CronJob/actions/testdata/job.yaml b/resource_customizations/batch/CronJob/actions/testdata/job.yaml index 83570da514dfe..cf0f92da24818 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/job.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/job.yaml @@ -1,9 +1,11 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: hello-123 - namespace: test-ns -spec: +- k8sOperation: create + unstructuredObj: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-00000000000 + namespace: test-ns + spec: template: spec: containers: diff --git a/server/application/application.go b/server/application/application.go index 5336e441b72af..4ba40ade5aa51 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2001,7 +2001,7 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque } obj, err = s.kubectl.GetResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace) data, _ := json.Marshal(obj) - fmt.Print("************************************ GetResource " + bytes.NewBuffer(data).String() + "*************************************") + fmt.Println("************************************ GetResource " + bytes.NewBuffer(data).String() + "*************************************") } if err != nil { diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 84987caa1daf1..051268438c67a 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -1,19 +1,21 @@ package lua import ( + "bytes" "fmt" "os" "path/filepath" "strings" "testing" - "github.com/argoproj/gitops-engine/pkg/diff" "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/util/cli" + "github.com/argoproj/argo-cd/v2/util/errors" + "github.com/argoproj/gitops-engine/pkg/diff" ) type testNormalizer struct{} @@ -23,6 +25,13 @@ func (t testNormalizer) Normalize(un *unstructured.Unstructured) error { return nil } switch un.GetKind() { + case "Job": + err := unstructured.SetNestedField(un.Object, map[string]interface{}{"name": "not sure why this works"}, "metadata") + if err != nil { + return fmt.Errorf("failed to normalize Job: %w", err) + } + } + switch un.GetKind() { case "DaemonSet", "Deployment", "StatefulSet": err := unstructured.SetNestedStringMap(un.Object, map[string]string{"kubectl.kubernetes.io/restartedAt": "0001-01-01T00:00:00Z"}, "spec", "template", "metadata", "annotations") if err != nil { @@ -102,6 +111,7 @@ func TestLuaResourceActionsScript(t *testing.T) { for i := range resourceTest.ActionTests { test := resourceTest.ActionTests[i] testName := fmt.Sprintf("actions/%s/%s", test.Action, test.InputPath) + t.Run(testName, func(t *testing.T) { vm := VM{ // Uncomment the following line if you need to use lua libraries debugging @@ -109,42 +119,57 @@ func TestLuaResourceActionsScript(t *testing.T) { // privileges that API server has. //UseOpenLibs: true, } - obj := getObj(filepath.Join(dir, test.InputPath)) - action, err := vm.GetResourceAction(obj, test.Action) + sourceObj := getObj(filepath.Join(dir, test.InputPath)) + action, err := vm.GetResourceAction(sourceObj, test.Action) + assert.NoError(t, err) assert.NoError(t, err) - results, err := vm.ExecuteResourceAction(obj, action.ActionLua) + impactedResources, err := vm.ExecuteResourceAction(sourceObj, action.ActionLua) assert.NoError(t, err) - for _, impactedResource := range results { + // Treat the Lua expected output as a list + expectedObjects := getExpectedObjectList(t, filepath.Join(dir, test.ExpectedOutputPath)) + + for _, impactedResource := range impactedResources { result := impactedResource.UnstructuredObj - if impactedResource.K8SOperation == "patch" { - // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource - assert.EqualValues(t, obj.GetKind(), result.GetKind()) - assert.EqualValues(t, obj.GetAPIVersion(), result.GetAPIVersion()) - assert.EqualValues(t, obj.GetName(), result.GetName()) - assert.EqualValues(t, obj.GetNamespace(), result.GetNamespace()) - - expectedObj := getObj(filepath.Join(dir, test.ExpectedOutputPath)) - // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. - diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) - assert.NoError(t, err) - if diffResult.Modified { - t.Error("Output does not match input:") - err = cli.PrintDiff(test.Action, expectedObj, result) - assert.NoError(t, err) - } - } else { - if impactedResource.K8SOperation == "create" { - t.Log("There is a create action - hooray! But I don't know how to test it yet") - t.Log(result.GetKind(), result.GetName(), result.GetNamespace()) + // The expected output is a list of objects + // Find the actual impacted resource in the expected output + expectedObj := findFirstMatchingItem(expectedObjects.Items, func(u unstructured.Unstructured) bool { + // Job's name is derived from the CronJob name, so the returned Job name is not actually equal to the testdata output name + // Considering the Job found in the testdata output if its name starts with CronJob name + // TODO: maybe this should use a normalizer function instead of hard-coding the Job specifics here + if result.GetKind() == "Job" && sourceObj.GetKind() == "CronJob" { + return u.GroupVersionKind() == result.GroupVersionKind() && strings.HasPrefix(u.GetName(), sourceObj.GetName()) && u.GetNamespace() == result.GetNamespace() + } else { + t.Log(u.GroupVersionKind(), result.GroupVersionKind(), u.GetName(), result.GetName(), u.GetNamespace(), result.GetNamespace()) + return u.GroupVersionKind() == result.GroupVersionKind() && u.GetName() == result.GetName() && u.GetNamespace() == result.GetNamespace() } + }) + assert.NotNil(t, expectedObj) + + switch impactedResource.K8SOperation { + case "patch": + // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource + assert.EqualValues(t, sourceObj.GroupVersionKind(), result.GroupVersionKind()) + assert.EqualValues(t, sourceObj.GetName(), result.GetName()) + assert.EqualValues(t, sourceObj.GetNamespace(), result.GetNamespace()) + case "create": + // no special logic to test for now + default: + t.Error("Operation not supported: " + impactedResource.K8SOperation) + } + // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. + diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) + assert.NoError(t, err) + if diffResult.Modified { + t.Error("Output does not match input:") + err = cli.PrintDiff(test.Action, expectedObj, result) + assert.NoError(t, err) } } - }) } @@ -152,3 +177,46 @@ func TestLuaResourceActionsScript(t *testing.T) { }) assert.Nil(t, err) } + +// Handling backward compatibility. +// The old-style actions return a single object in the expected output from testdata, so will wrap them in a list +func getExpectedObjectList(t *testing.T, path string) *unstructured.UnstructuredList { + yamlBytes, err := os.ReadFile(path) + errors.CheckError(err) + unstructuredList := &unstructured.UnstructuredList{} + yamlString := bytes.NewBuffer(yamlBytes).String() + if yamlString[0] == '-' { + // The string represents a new-style action array output, where each member is a wrapper around a k8s unstructured resource + objList := make([]map[string]interface{}, 5) + err = yaml.Unmarshal(yamlBytes, &objList) + errors.CheckError(err) + unstructuredList.Items = make([]unstructured.Unstructured, len(objList)) + // Append each map in objList to the Items field of the new object + for i, obj := range objList { + unstructuredObj, ok := obj["unstructuredObj"].(map[string]interface{}) + if !ok { + t.Error("Wrong type of unstructuredObj") + } + unstructuredList.Items[i] = unstructured.Unstructured{Object: unstructuredObj} + } + } else { + // The string represents an old-style action object output, which is a k8s unstructured resource + obj := make(map[string]interface{}) + err = yaml.Unmarshal(yamlBytes, &obj) + errors.CheckError(err) + unstructuredList.Items = make([]unstructured.Unstructured, 1) + unstructuredList.Items[0] = unstructured.Unstructured{Object: obj} + } + return unstructuredList +} + +func findFirstMatchingItem(items []unstructured.Unstructured, f func(unstructured.Unstructured) bool) *unstructured.Unstructured { + var matching *unstructured.Unstructured = nil + for _, item := range items { + if f(item) { + matching = &item + break + } + } + return matching +} diff --git a/util/lua/health_test.go b/util/lua/health_test.go index dad4696388817..ffc2264241b7d 100644 --- a/util/lua/health_test.go +++ b/util/lua/health_test.go @@ -1,8 +1,6 @@ package lua import ( - "bytes" - "fmt" "os" "path/filepath" "strings" @@ -27,11 +25,11 @@ type IndividualTest struct { func getObj(path string) *unstructured.Unstructured { yamlBytes, err := os.ReadFile(path) - fmt.Print("************************************ input " + bytes.NewBuffer(yamlBytes).String() + "*************************************") errors.CheckError(err) obj := make(map[string]interface{}) err = yaml.Unmarshal(yamlBytes, &obj) errors.CheckError(err) + return &unstructured.Unstructured{Object: obj} } diff --git a/util/lua/impacted_resource.go b/util/lua/impacted_resource.go new file mode 100644 index 0000000000000..dadddf3178b6f --- /dev/null +++ b/util/lua/impacted_resource.go @@ -0,0 +1,17 @@ +package lua + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// This struct represents a wrapper, that is returned from Lua custom action script, around the unstructured k8s resource + a k8s verb +// that will need to be performed on this returned resource. +// Currently only "create" and "patch" operations are supported for custom actions +// This replaces the traditional architecture of "Lua action returns the source resource for ArgoCD to patch". +// This enables ArgoCD to create NEW resources upon custom action. +// Note that the Lua code in the custom action is coupled to this type, since Lua json output is then unmarshalled to this struct. +// TODO: maybe K8SOperation needs to be an enum of supported k8s verbs, with a custom json marshaller/unmarshaller +type ImpactedResource struct { + UnstructuredObj *unstructured.Unstructured + K8SOperation string +} diff --git a/util/lua/lua.go b/util/lua/lua.go index 83aeb852c22af..1c3e8aed024c0 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -58,17 +58,6 @@ type VM struct { UseOpenLibs bool } -// This struct represents a resource, that is returned from Lua custom action script, along with a k8s verb -// that will need to be performed on this returned resource. -// This replaces the traditional architecture of "Lua action returns a resource that ArgoCD will patch". -// This enables ArgoCD to create NEW resources upon custom actions. -// Note that the Lua code in the custom action is coupled to this type, and must return a json output with exactly those fields, -// since the json output is then unmarshalled to this struct. -type ImpactedResource struct { - UnstructuredObj *unstructured.Unstructured - K8SOperation string -} - func (vm VM) runLua(obj *unstructured.Unstructured, script string) (*lua.LState, error) { l := lua.NewState(lua.Options{ SkipOpenLibs: !vm.UseOpenLibs, @@ -176,8 +165,6 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string var impactedResources []ImpactedResource jsonString := bytes.NewBuffer(jsonBytes).String() - fmt.Print("************************************" + jsonString + "*************************************") - // The output from Lua is either an object (old-style action output) or an array (new-style action output). // Check whether the string starts with an opening square bracket and ends with a closing square bracket, // avoiding programming by exception. From abdae1b0a0b37e52912e5bf8b386edfee604ef93 Mon Sep 17 00:00:00 2001 From: reggie Date: Mon, 20 Mar 2023 23:07:53 +0200 Subject: [PATCH 20/38] e2e tests Signed-off-by: reggie --- cmd/argocd/commands/admin/settings.go | 5 +- cmd/argocd/commands/admin/settings_test.go | 65 +++++++++- server/application/application.go | 6 - server/application/application_test.go | 77 ++++++++++- test/container/Dockerfile | 6 +- test/e2e/app_management_test.go | 61 ++++++++- .../testdata/resource-actions/cron-job.yaml | 1 - util/lua/custom_actions_test.go | 1 - util/lua/lua.go | 9 +- util/lua/lua_test.go | 122 +++++++++++++++++- 10 files changed, 332 insertions(+), 21 deletions(-) diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index 6996353f81b7e..b80b32e439c03 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -557,7 +557,10 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a _, _ = fmt.Printf("Following fields have been changed:\n\n") _ = cli.PrintDiff(res.GetName(), &res, result) case "create": - _, _ = fmt.Printf("Create action detected. Don't know what to print yet") + yamlBytes, err := yaml.Marshal(impactedResource.UnstructuredObj) + errors.CheckError(err) + fmt.Println("Following resource was created:") + fmt.Println(bytes.NewBuffer(yamlBytes).String()) default: errors.CheckError(fmt.Errorf("Unsupported operation: %s", impactedResource.K8SOperation)) } diff --git a/cmd/argocd/commands/admin/settings_test.go b/cmd/argocd/commands/admin/settings_test.go index 696387d0e01fc..71dcbd06feb33 100644 --- a/cmd/argocd/commands/admin/settings_test.go +++ b/cmd/argocd/commands/admin/settings_test.go @@ -233,6 +233,17 @@ spec: replicas: 0` ) +const ( + testCronJobYAML = `apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: test-ns + uid: "123" +spec: + schedule: "* * * * *"` +) + func tempFile(content string) (string, io.Closer, error) { f, err := os.CreateTemp("", "*.yaml") if err != nil { @@ -342,6 +353,12 @@ func TestResourceOverrideAction(t *testing.T) { } defer utils.Close(closer) + cronJobFile, closer, err := tempFile(testCronJobYAML) + if !assert.NoError(t, err) { + return + } + defer utils.Close(closer) + t.Run("NoActions", func(t *testing.T) { cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ "resource.customizations": `apps/Deployment: {}`})) @@ -354,7 +371,7 @@ func TestResourceOverrideAction(t *testing.T) { assert.Contains(t, out, "Actions are not configured") }) - t.Run("ActionConfigured", func(t *testing.T) { + t.Run("OldStyleActionConfigured", func(t *testing.T) { cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ "resource.customizations": `apps/Deployment: actions: | @@ -388,4 +405,50 @@ restart false resume false `) }) + + t.Run("NewStyleActionConfigured", func(t *testing.T) { + cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ + "resource.customizations": `batch/CronJob: + actions: | + discovery.lua: | + actions = {} + actions["create-a-job"] = {["disabled"] = false} + return actions + definitions: + - name: test + action.lua: | + job1 = {} + job1.apiVersion = "batch/v1" + job1.kind = "Job" + job1.metadata = {} + job1.metadata.name = "hello-1" + job1.metadata.namespace = "test-ns" + impactedResource1 = {} + impactedResource1.k8sOperation = "create" + impactedResource1.unstructuredObj = job1 + result = {} + result[1] = impactedResource1 + return result +`})) + out, err := captureStdout(func() { + cmd.SetArgs([]string{"run-action", cronJobFile, "test"}) + err := cmd.Execute() + assert.NoError(t, err) + }) + assert.NoError(t, err) + assert.Contains(t, out, "resource was created:") + assert.Contains(t, out, "hello-1") + + out, err = captureStdout(func() { + cmd.SetArgs([]string{"list-actions", cronJobFile}) + err := cmd.Execute() + assert.NoError(t, err) + }) + + assert.NoError(t, err) + assert.Contains(t, out, "NAME") + assert.Contains(t, out, "ENABLED") + assert.Contains(t, out, "create-a-job") + assert.Contains(t, out, "false") + }) } diff --git a/server/application/application.go b/server/application/application.go index 4ba40ade5aa51..31b1d9e453e65 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -1,7 +1,6 @@ package application import ( - "bytes" "context" "encoding/json" "errors" @@ -1990,9 +1989,6 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque if err != nil { return nil, nil, nil, nil, fmt.Errorf("error getting application cluster config: %w", err) } - data, _ := json.Marshal(obj) - fmt.Print("************************************ app " + bytes.NewBuffer(data).String() + "*************************************") - obj, err = kube.ToUnstructured(app) } else { res, config, app, err = s.getAppLiveResource(ctx, rbacRequest, q) @@ -2000,8 +1996,6 @@ func (s *Server) getUnstructuredLiveResourceOrApp(ctx context.Context, rbacReque return nil, nil, nil, nil, fmt.Errorf("error getting app live resource: %w", err) } obj, err = s.kubectl.GetResource(ctx, config, res.GroupKindVersion(), res.Name, res.Namespace) - data, _ := json.Marshal(obj) - fmt.Println("************************************ GetResource " + bytes.NewBuffer(data).String() + "*************************************") } if err != nil { diff --git a/server/application/application_test.go b/server/application/application_test.go index 7bede0e656982..f1594f4fc5654 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1242,7 +1242,19 @@ func returnCronJob() *unstructured.Unstructured { }} } -func TestRunResourceAction(t *testing.T) { +func returnDeployment() *unstructured.Unstructured { + return &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "nginx-deploy", + "namespace": testNamespace, + }, + "spec": map[string]interface{}{}, + }} +} + +func TestRunNewStyleResourceAction(t *testing.T) { cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) group := "batch" @@ -1340,3 +1352,66 @@ func TestRunResourceAction(t *testing.T) { assert.NotNil(t, appResponse) }) } + +func TestRunOldStyleResourceAction(t *testing.T) { + cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) + + group := "apps" + kind := "Deployment" + version := "v1" + resourceName := "nginx-deploy" + namespace := testNamespace + action := "pause" + uid := "2" + + resources := []appsv1.ResourceStatus{{ + Group: group, + Kind: kind, + Name: resourceName, + Namespace: testNamespace, + Version: version, + }} + + getResourceFunc := func(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string) (*unstructured.Unstructured, error) { + return returnDeployment(), nil + } + + appStateCache := appstate.NewCache(cacheClient, time.Minute) + + nodes := []appsv1.ResourceNode{{ + ResourceRef: appsv1.ResourceRef{ + Group: group, + Kind: kind, + Version: version, + Name: resourceName, + Namespace: testNamespace, + UID: uid, + }, + }} + + t.Run("DefaultPatchOperation", func(t *testing.T) { + testApp := newTestApp() + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources + + appServer := newTestAppServerWithResourceFunc(&getResourceFunc, testApp) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) + + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) + + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + require.NoError(t, runErr) + assert.NotNil(t, appResponse) + }) +} diff --git a/test/container/Dockerfile b/test/container/Dockerfile index dd82b0eaceabc..702c6954585e0 100644 --- a/test/container/Dockerfile +++ b/test/container/Dockerfile @@ -84,7 +84,7 @@ COPY --from=registry /etc/docker/registry/config.yml /etc/docker/registry/config # Copy node binaries COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=node /usr/local/bin/node /usr/local/bin -COPY --from=node /opt/yarn-v1.22.4 /opt/yarn-v1.22.4 +COPY --from=node /opt/yarn-v1.22.19 /opt/yarn-v1.22.19 # Entrypoint is required for container's user management COPY ./test/container/entrypoint.sh /usr/local/bin @@ -110,8 +110,8 @@ RUN useradd -l -u ${UID} -d /home/user -s /bin/bash user && \ ln -s /usr/local/bin/node /usr/local/bin/nodejs && \ ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm && \ ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx && \ - ln -s /opt/yarn-v1.22.4/bin/yarn /usr/local/bin/yarn && \ - ln -s /opt/yarn-v1.22.4/bin/yarnpkg /usr/local/bin/yarnpkg && \ + ln -s /opt/yarn-v1.22.19/bin/yarn /usr/local/bin/yarn && \ + ln -s /opt/yarn-v1.22.19/bin/yarnpkg /usr/local/bin/yarnpkg && \ mkdir -p /var/lib/registry ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index 0f00a08e815be..62adea7ccc344 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -48,6 +48,7 @@ const ( guestbookPathLocal = "./testdata/guestbook_local" globalWithNoNameSpace = "global-with-no-namespace" guestbookWithNamespace = "guestbook-with-namespace" + resourceActions = "resource-actions" appLogsRetryCount = 5 ) @@ -879,7 +880,7 @@ definitions: obj.metadata.labels.sample = 'test' return obj` -func TestResourceAction(t *testing.T) { +func TestOldStyleResourceAction(t *testing.T) { Given(t). Path(guestbookPath). ResourceOverrides(map[string]ResourceOverride{"apps/Deployment": {Actions: actionsConfig}}). @@ -921,6 +922,64 @@ func TestResourceAction(t *testing.T) { }) } +const newStyleActionsConfig = `discovery.lua: return { sample = {} } +definitions: +- name: sample + action.lua: | + job1 = {} + job1.apiVersion = "batch/v1" + job1.kind = "Job" + job1.metadata = {} + job1.metadata.name = "hello-1" + impactedResource1 = {} + impactedResource1.k8sOperation = "create" + impactedResource1.unstructuredObj = job1 + result = {} + result[1] = impactedResource1 + return result` + +func TestNewStyleResourceAction(t *testing.T) { + Given(t). + Path(resourceActions). + ResourceOverrides(map[string]ResourceOverride{"batch/CronJob": {Actions: newStyleActionsConfig}}). + When(). + CreateApp(). + Sync(). + Then(). + And(func(app *Application) { + + closer, client, err := ArgoCDClientset.NewApplicationClient() + assert.NoError(t, err) + defer io.Close(closer) + + actions, err := client.ListResourceActions(context.Background(), &applicationpkg.ApplicationResourceRequest{ + Name: &app.Name, + Group: pointer.String("batch"), + Kind: pointer.String("CronJob"), + Version: pointer.String("v1"), + Namespace: pointer.String(DeploymentNamespace()), + ResourceName: pointer.String("hello"), + }) + assert.NoError(t, err) + assert.Equal(t, []*ResourceAction{{Name: "sample", Disabled: false}}, actions.Actions) + + _, err = client.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{Name: &app.Name, + Group: pointer.String("batch"), + Kind: pointer.String("CronJob"), + Version: pointer.String("v1"), + Namespace: pointer.String(DeploymentNamespace()), + ResourceName: pointer.String("hello"), + Action: pointer.String("sample"), + }) + assert.NoError(t, err) + + deployment, err := KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-1", metav1.GetOptions{}) + assert.NoError(t, err) + + assert.Equal(t, "test", deployment.Labels["sample"]) + }) +} + func TestSyncResourceByLabel(t *testing.T) { Given(t). Path(guestbookPath). diff --git a/test/e2e/testdata/resource-actions/cron-job.yaml b/test/e2e/testdata/resource-actions/cron-job.yaml index 58f56de94232e..3ab1fb9b1cd8a 100644 --- a/test/e2e/testdata/resource-actions/cron-job.yaml +++ b/test/e2e/testdata/resource-actions/cron-job.yaml @@ -2,7 +2,6 @@ apiVersion: batch/v1 kind: CronJob metadata: name: hello - namespace: spec: schedule: "* * * * *" jobTemplate: diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 051268438c67a..9feca174afcc7 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -143,7 +143,6 @@ func TestLuaResourceActionsScript(t *testing.T) { if result.GetKind() == "Job" && sourceObj.GetKind() == "CronJob" { return u.GroupVersionKind() == result.GroupVersionKind() && strings.HasPrefix(u.GetName(), sourceObj.GetName()) && u.GetNamespace() == result.GetNamespace() } else { - t.Log(u.GroupVersionKind(), result.GroupVersionKind(), u.GetName(), result.GetName(), u.GetNamespace(), result.GetNamespace()) return u.GroupVersionKind() == result.GroupVersionKind() && u.GetName() == result.GetName() && u.GetNamespace() == result.GetNamespace() } }) diff --git a/util/lua/lua.go b/util/lua/lua.go index 1c3e8aed024c0..73f0c30483d82 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -165,12 +165,17 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string var impactedResources []ImpactedResource jsonString := bytes.NewBuffer(jsonBytes).String() + if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { + if len(jsonString) < 2 { + return nil, fmt.Errorf("Lua output was not a valid json object or array") + } + } // The output from Lua is either an object (old-style action output) or an array (new-style action output). // Check whether the string starts with an opening square bracket and ends with a closing square bracket, // avoiding programming by exception. if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { // The string represents a new-style action array output - impactedResources, err = unmarshalToImpactedResources(string(jsonBytes)) + impactedResources, err = UnmarshalToImpactedResources(string(jsonBytes)) if err != nil { return nil, err } @@ -198,7 +203,7 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string } // UnmarshalToImpactedResources unmarshals an ImpactedResource array representation in JSON to ImpactedResource array -func unmarshalToImpactedResources(resources string) ([]ImpactedResource, error) { +func UnmarshalToImpactedResources(resources string) ([]ImpactedResource, error) { if resources == "" || resources == "null" { return nil, nil } diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index f30087e290868..0ef9e4678acc2 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -1,6 +1,7 @@ package lua import ( + "bytes" "fmt" "testing" @@ -24,6 +25,7 @@ metadata: namespace: default resourceVersion: "123" ` + const objWithNoScriptJSON = ` apiVersion: not-an-endpoint.io/v1alpha1 kind: Test @@ -370,7 +372,7 @@ obj.metadata.labels["test"] = "test" return obj ` -const expectedUpdatedObj = ` +const expectedLuaUpdatedResult = ` apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: @@ -382,15 +384,127 @@ metadata: resourceVersion: "123" ` -func TestExecuteResourceAction(t *testing.T) { +// Test an action that returns a single k8s resource json +func TestExecuteOldStyleResourceAction(t *testing.T) { testObj := StrToUnstructured(objJSON) - expectedObj := StrToUnstructured(expectedUpdatedObj) + expectedLuaUpdatedObj := StrToUnstructured(expectedLuaUpdatedResult) vm := VM{} newObjects, err := vm.ExecuteResourceAction(testObj, validActionLua) assert.Nil(t, err) assert.Equal(t, len(newObjects), 1) assert.Equal(t, newObjects[0].K8SOperation, "patch") - assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj) + assert.Equal(t, expectedLuaUpdatedObj, newObjects[0].UnstructuredObj) +} + +const cronJobObjYaml = ` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: test-ns +` + +const expectedCreatedJobObjList = ` +- k8sOperation: create + unstructuredObj: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-1 + namespace: test-ns +` + +const expectedCreatedMultipleJobsObjList = ` +- k8sOperation: create + unstructuredObj: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-1 + namespace: test-ns +- k8sOperation: create + unstructuredObj: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-2 + namespace: test-ns +` + +const createJobActionLua = ` +job = {} +job.apiVersion = "batch/v1" +job.kind = "Job" + +job.metadata = {} +job.metadata.name = "hello-1" +job.metadata.namespace = "test-ns" + +impactedResource = {} +impactedResource.k8sOperation = "create" +impactedResource.unstructuredObj = job +result = {} +result[1] = impactedResource + +return result +` + +const createMultipleJobsActionLua = ` +job1 = {} +job1.apiVersion = "batch/v1" +job1.kind = "Job" + +job1.metadata = {} +job1.metadata.name = "hello-1" +job1.metadata.namespace = "test-ns" + +impactedResource1 = {} +impactedResource1.k8sOperation = "create" +impactedResource1.unstructuredObj = job1 +result = {} +result[1] = impactedResource1 + +job2 = {} +job2.apiVersion = "batch/v1" +job2.kind = "Job" + +job2.metadata = {} +job2.metadata.name = "hello-2" +job2.metadata.namespace = "test-ns" + +impactedResource2 = {} +impactedResource2.k8sOperation = "create" +impactedResource2.unstructuredObj = job2 + +result[2] = impactedResource2 + +return result +` + +func TestExecuteNewStyleCreateActionSingleResource(t *testing.T) { + testObj := StrToUnstructured(cronJobObjYaml) + jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedJobObjList)) + assert.Nil(t, err) + t.Log(bytes.NewBuffer(jsonBytes).String()) + expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String()) + assert.Nil(t, err) + vm := VM{} + newObjects, err := vm.ExecuteResourceAction(testObj, createJobActionLua) + assert.Nil(t, err) + assert.Equal(t, expectedObjects, newObjects) +} + +func TestExecuteNewStyleCreateActionMultipleResources(t *testing.T) { + testObj := StrToUnstructured(cronJobObjYaml) + jsonBytes, err := yaml.YAMLToJSON([]byte(expectedCreatedMultipleJobsObjList)) + assert.Nil(t, err) + // t.Log(bytes.NewBuffer(jsonBytes).String()) + expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String()) + assert.Nil(t, err) + vm := VM{} + newObjects, err := vm.ExecuteResourceAction(testObj, createMultipleJobsActionLua) + assert.Nil(t, err) + assert.Equal(t, expectedObjects, newObjects) } func TestExecuteResourceActionNonTableReturn(t *testing.T) { From 9429865e0382044f7ec7660aae1d9d18f60e055b Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 21 Mar 2023 19:02:06 +0200 Subject: [PATCH 21/38] json marshaling annotations ImpactedResource, e2e tests and docs Signed-off-by: reggie --- cmd/argocd/commands/admin/settings_test.go | 4 +- docs/operator-manual/resource_actions.md | 9 +++ .../CronJob/actions/create-job/action.lua | 4 +- server/application/application.go | 2 +- test/e2e/app_management_test.go | 78 +++++++++++++++---- util/lua/impacted_resource.go | 4 +- util/lua/lua_test.go | 24 +++--- 7 files changed, 90 insertions(+), 35 deletions(-) diff --git a/cmd/argocd/commands/admin/settings_test.go b/cmd/argocd/commands/admin/settings_test.go index 71dcbd06feb33..b1f14d7cee82b 100644 --- a/cmd/argocd/commands/admin/settings_test.go +++ b/cmd/argocd/commands/admin/settings_test.go @@ -424,8 +424,8 @@ resume false job1.metadata.name = "hello-1" job1.metadata.namespace = "test-ns" impactedResource1 = {} - impactedResource1.k8sOperation = "create" - impactedResource1.unstructuredObj = job1 + impactedResource1.operation = "create" + impactedResource1.resource = job1 result = {} result[1] = impactedResource1 return result diff --git a/docs/operator-manual/resource_actions.md b/docs/operator-manual/resource_actions.md index 2dbe58749cad4..17cfc9b9bb181 100644 --- a/docs/operator-manual/resource_actions.md +++ b/docs/operator-manual/resource_actions.md @@ -14,6 +14,15 @@ Argo CD supports custom resource actions written in [Lua](https://www.lua.org/). You can define your own custom resource actions in the `argocd-cm` ConfigMap. +### Custom Resource Action Types + +#### An action that modifies the source resource +This action modifies and returns the source resource. + +#### An action that produces a list of new or modified resources +This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to invoke on. +Currently supported operations are "create" and "patch". + ### Define a Custom Resource Action in `argocd-cm` ConfigMap Custom resource actions can be defined in `resource.customizations.actions.` field of `argocd-cm`. Following example demonstrates a set of custom actions for `CronJob` resources. diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index 6489a66e827a7..119e0d35eccb6 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -50,8 +50,8 @@ job.spec.template = {} job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) impactedResource = {} -impactedResource.k8sOperation = "create" -impactedResource.unstructuredObj = job +impactedResource.operation = "create" +impactedResource.resource = job result = {} result[1] = impactedResource diff --git a/server/application/application.go b/server/application/application.go index 31b1d9e453e65..605e1ec5c4793 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2156,7 +2156,7 @@ func (s *Server) createResource(ctx context.Context, config *rest.Config, app *a return nil, fmt.Errorf("error checking resource permissions: %w", err) } if !permitted { - return nil, fmt.Errorf("%s named %s's creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) + return nil, fmt.Errorf("%s named %s creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) } // Create the resource diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index 62adea7ccc344..245b0de3bd98a 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -926,22 +926,70 @@ const newStyleActionsConfig = `discovery.lua: return { sample = {} } definitions: - name: sample action.lua: | - job1 = {} - job1.apiVersion = "batch/v1" - job1.kind = "Job" - job1.metadata = {} - job1.metadata.name = "hello-1" - impactedResource1 = {} - impactedResource1.k8sOperation = "create" - impactedResource1.unstructuredObj = job1 - result = {} - result[1] = impactedResource1 - return result` - -func TestNewStyleResourceAction(t *testing.T) { + local os = require("os") + + function deepCopy(object) + local lookup_table = {} + local function _copy(obj) + if type(obj) ~= "table" then + return obj + elseif lookup_table[obj] then + return lookup_table[obj] + elseif next(obj) == nil then + return nil + else + local new_table = {} + lookup_table[obj] = new_table + for key, value in pairs(obj) do + new_table[_copy(key)] = _copy(value) + end + return setmetatable(new_table, getmetatable(obj)) + end + end + return _copy(object) + end + + job = {} + job.apiVersion = "batch/v1" + job.kind = "Job" + + job.metadata = {} + job.metadata.name = obj.metadata.name .. "-123" + job.metadata.namespace = obj.metadata.namespace + + ownerRef = {} + ownerRef.apiVersion = obj.apiVersion + ownerRef.kind = obj.kind + ownerRef.name = obj.metadata.name + ownerRef.uid = obj.metadata.uid + job.metadata.ownerReferences = {} + job.metadata.ownerReferences[1] = ownerRef + + job.spec = {} + job.spec.suspend = false + job.spec.template = {} + job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) + + impactedResource123 = {} + impactedResource123.operation = "create" + impactedResource123.resource = job + result = {} + result[1] = impactedResource123 + + return result` + +func TestNewStyleResourceActionPermitted(t *testing.T) { Given(t). Path(resourceActions). ResourceOverrides(map[string]ResourceOverride{"batch/CronJob": {Actions: newStyleActionsConfig}}). + ProjectSpec(AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, + NamespaceResourceWhitelist: []metav1.GroupKind{ + {Group: "batch", Kind: "Job"}, + {Group: "batch", Kind: "CronJob"}, + {Group: "", Kind: "Pod"}, + }}). When(). CreateApp(). Sync(). @@ -973,10 +1021,8 @@ func TestNewStyleResourceAction(t *testing.T) { }) assert.NoError(t, err) - deployment, err := KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-1", metav1.GetOptions{}) + _, err = KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-123", metav1.GetOptions{}) assert.NoError(t, err) - - assert.Equal(t, "test", deployment.Labels["sample"]) }) } diff --git a/util/lua/impacted_resource.go b/util/lua/impacted_resource.go index dadddf3178b6f..63fc0165523be 100644 --- a/util/lua/impacted_resource.go +++ b/util/lua/impacted_resource.go @@ -12,6 +12,6 @@ import ( // Note that the Lua code in the custom action is coupled to this type, since Lua json output is then unmarshalled to this struct. // TODO: maybe K8SOperation needs to be an enum of supported k8s verbs, with a custom json marshaller/unmarshaller type ImpactedResource struct { - UnstructuredObj *unstructured.Unstructured - K8SOperation string + UnstructuredObj *unstructured.Unstructured `json:"resource"` + K8SOperation string `json:"operation"` } diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index 0ef9e4678acc2..27f35f480c468 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -405,8 +405,8 @@ metadata: ` const expectedCreatedJobObjList = ` -- k8sOperation: create - unstructuredObj: +- operation: create + resource: apiVersion: batch/v1 kind: Job metadata: @@ -415,15 +415,15 @@ const expectedCreatedJobObjList = ` ` const expectedCreatedMultipleJobsObjList = ` -- k8sOperation: create - unstructuredObj: +- operation: create + resource: apiVersion: batch/v1 kind: Job metadata: name: hello-1 namespace: test-ns -- k8sOperation: create - unstructuredObj: +- operation: create + resource: apiVersion: batch/v1 kind: Job metadata: @@ -441,8 +441,8 @@ job.metadata.name = "hello-1" job.metadata.namespace = "test-ns" impactedResource = {} -impactedResource.k8sOperation = "create" -impactedResource.unstructuredObj = job +impactedResource.operation = "create" +impactedResource.resource = job result = {} result[1] = impactedResource @@ -459,8 +459,8 @@ job1.metadata.name = "hello-1" job1.metadata.namespace = "test-ns" impactedResource1 = {} -impactedResource1.k8sOperation = "create" -impactedResource1.unstructuredObj = job1 +impactedResource1.operation = "create" +impactedResource1.resource = job1 result = {} result[1] = impactedResource1 @@ -473,8 +473,8 @@ job2.metadata.name = "hello-2" job2.metadata.namespace = "test-ns" impactedResource2 = {} -impactedResource2.k8sOperation = "create" -impactedResource2.unstructuredObj = job2 +impactedResource2.operation = "create" +impactedResource2.resource = job2 result[2] = impactedResource2 From 063cacf3fa33e39083f00b5fffe89d1de3b8b065 Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 24 Mar 2023 19:57:59 +0300 Subject: [PATCH 22/38] more docs and tests Signed-off-by: reggie --- cmd/argocd/commands/admin/settings_test.go | 2 +- docs/operator-manual/resource_actions.md | 105 ++++++++++++++++++- server/application/application.go | 38 ++++--- test/e2e/app_management_test.go | 114 ++++++++++++++++++++- util/lua/lua.go | 9 ++ util/lua/lua_test.go | 93 +++++++++++++++++ 6 files changed, 340 insertions(+), 21 deletions(-) diff --git a/cmd/argocd/commands/admin/settings_test.go b/cmd/argocd/commands/admin/settings_test.go index b1f14d7cee82b..387a2c32c7f95 100644 --- a/cmd/argocd/commands/admin/settings_test.go +++ b/cmd/argocd/commands/admin/settings_test.go @@ -422,7 +422,7 @@ resume false job1.kind = "Job" job1.metadata = {} job1.metadata.name = "hello-1" - job1.metadata.namespace = "test-ns" + job1.metadata.namespace = "obj.metadata.namespace" impactedResource1 = {} impactedResource1.operation = "create" impactedResource1.resource = job1 diff --git a/docs/operator-manual/resource_actions.md b/docs/operator-manual/resource_actions.md index 17cfc9b9bb181..46f9542aaf0c2 100644 --- a/docs/operator-manual/resource_actions.md +++ b/docs/operator-manual/resource_actions.md @@ -12,20 +12,27 @@ Argo CD supports custom resource actions written in [Lua](https://www.lua.org/). * Have a custom resource for which Argo CD does not provide any built-in actions. * Have a commonly performed manual task that might be error prone if executed by users via `kubectl` +The resource actions act on a single object. + You can define your own custom resource actions in the `argocd-cm` ConfigMap. ### Custom Resource Action Types #### An action that modifies the source resource This action modifies and returns the source resource. +This kind of action was the only one available till 2.8, and it is still supported. #### An action that produces a list of new or modified resources -This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to invoke on. -Currently supported operations are "create" and "patch". +An alpha feature, introduced in 2.8. +This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to perform on. +Currently supported operations are "create" and "patch", "patch" is only supported for the source resource. +Creating new resources is possible, by specifying a "create" operation for each such resource in the returned list. +One of the returned resources can be the modified source object, with a "patch" operation, if needed. +See the definition examples below. ### Define a Custom Resource Action in `argocd-cm` ConfigMap -Custom resource actions can be defined in `resource.customizations.actions.` field of `argocd-cm`. Following example demonstrates a set of custom actions for `CronJob` resources. +Custom resource actions can be defined in `resource.customizations.actions.` field of `argocd-cm`. Following example demonstrates a set of custom actions for `CronJob` resources, each such action returns the modified CronJob. The customizations key is in the format of `resource.customizations.actions.`. ```yaml @@ -60,4 +67,94 @@ resource.customizations.actions.batch_CronJob: | The `discovery.lua` script must return a table where the key name represents the action name. You can optionally include logic to enable or disable certain actions based on the current object state. -Each action name must be represented in the list of `definitions` with an accompanying `action.lua` script to control the resource modifications. The `obj` is a global variable which contains the resource. Each action script must return an optionally modified version of the resource. In this example, we are simply setting `.spec.suspend` to either `true` or `false`. +Each action name must be represented in the list of `definitions` with an accompanying `action.lua` script to control the resource modifications. The `obj` is a global variable which contains the resource. Each action script returns an optionally modified version of the resource. In this example, we are simply setting `.spec.suspend` to either `true` or `false`. + +#### Creating new resources with a custom action +The resource the action is invoked on would be referred to as the `source resource`. +The new resource must be permitted on the AppProject level, otherwise the creation will fail. +If the new resource represents a k8s child of the source resource, the ownerReference should be set on the new resource. +Here is an example Lua snippet, that takes care of constructing a Job resource that is a child of a source CronJob resource - the `obj` is a global variable, which contains the source resource: +``` +... +ownerRef = {} +ownerRef.apiVersion = obj.apiVersion +ownerRef.kind = obj.kind +ownerRef.name = obj.metadata.name +ownerRef.uid = obj.metadata.uid +job = {} +job.metadata = {} +job.metadata.ownerReferences = {} +job.metadata.ownerReferences[1] = ownerRef +... +``` + +If the new resource is independent of the source resource, the default behavior of such new resource is that it is not known by the App of the source resource (as it is not part of the desired state and does not have an `ownerReference`). +To make the App aware of the new resource, the `app.kubernetes.io/instance` label (or other ArgoCD tracking label, if configured) must be set on the resource. It can be copied from the source resource, like this: +``` +... +newObj = {} +newObj.metadata = {} +newObj.metadata.labels = {} +newObj.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] +... +``` +While the new resource will be part of the App with the tracking label in place, it will be immediately pruned if auto prune is set on the App. To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: +``` +... +newObj.metadata.annotations = {} +newObj.metadata.annotations["argocd.argoproj.io/sync-options"] = "Prune=false" +... +``` +(If setting `Prune=false` behavior, the resource will not be deleted upon the deletion of the App, and will require a manual cleanup.) + +The resource and the App will now appear out of sync - which is the expected ArgoCD behavior upon creating a resource that is not part of the desired state. + +If you wish to treat such an App as a synced one, add the following resource annotation in Lua code: +``` +... +newObj.metadata.annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" +... +``` + +#### An action that produces a list of resources - a complete example: +```yaml +resource.customizations.actions.ConfigMap: | + discovery.lua: | + actions = {} + actions["do-things"] = {} + return actions + definitions: + - name: do-things + action.lua: | + # Create a new ConfigMap + cm1 = {} + cm1.apiVersion = "v1" + cm1.kind = "ConfigMap" + cm1.metadata = {} + cm1.metadata.name = "cm1" + cm1.metadata.namespace = obj.metadata.namespace + cm1.metadata.labels = {} + # Copy ArgoCD tracking label so that the resource is recognized by the App + cm1.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] + cm1.metadata.annotations = {} + # For Apps with auto-prune, set the prune false on the resource, so it does not get deleted + cm1.metadata.annotations["argocd.argoproj.io/sync-options"] = "Prune=false" + # Keep the App synced even though it has a resource that is not in Git + cm1.metadata.annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" + cm1.data = {} + cm1.data.myKey1 = "myValue1" + impactedResource1 = {} + impactedResource1.operation = "create" + impactedResource1.resource = cm1 + + # Patch the original cm + obj.metadata.label["aKey"] = "aValue" + impactedResource2 = {} + impactedResource2.operation = "patch" + impactedResource2.resource = obj + + result = {} + result[1] = impactedResource1 + result[2] = impactedResource2 + return result +``` \ No newline at end of file diff --git a/server/application/application.go b/server/application/application.go index 605e1ec5c4793..9a5fcdd0b0648 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2046,6 +2046,11 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, err } + liveObjBytes, err := json.Marshal(liveObj) + if err != nil { + return nil, fmt.Errorf("error marshaling live object: %w", err) + } + resourceOverrides, err := s.settingsMgr.GetResourceOverrides() if err != nil { return nil, fmt.Errorf("error getting resource overrides: %w", err) @@ -2064,16 +2069,21 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error executing Lua resource action: %w", err) } + // First, make sure all the returned resources are permitted, no matter for which operation. for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj - newObjBytes, err := json.Marshal(newObj) + err := s.verifyResourcePermitted(ctx, app, newObj) if err != nil { - return nil, fmt.Errorf("error marshaling new object: %w", err) + return nil, err } + } - liveObjBytes, err := json.Marshal(liveObj) + // Now, perform the actual operations + for _, impactedResource := range newObjects { + newObj := impactedResource.UnstructuredObj + newObjBytes, err := json.Marshal(newObj) if err != nil { - return nil, fmt.Errorf("error marshaling live object: %w", err) + return nil, fmt.Errorf("error marshaling new object: %w", err) } switch impactedResource.K8SOperation { @@ -2081,8 +2091,8 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) case "create": return s.createResource(ctx, config, app, newObj) - default: - return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) + + // No default case since a not supported operation fails upon luaVM.ExecuteResourceAction } } @@ -2138,14 +2148,14 @@ func (s *Server) patchResource(ctx context.Context, config *rest.Config, liveObj return &application.ApplicationResponse{}, nil } -func (s *Server) createResource(ctx context.Context, config *rest.Config, app *appv1.Application, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { +func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Application, obj *unstructured.Unstructured) error { proj, err := argo.GetAppProject(app, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx) if err != nil { if apierr.IsNotFound(err) { - return nil, fmt.Errorf("application references project %s which does not exist", app.Spec.Project) + return fmt.Errorf("application references project %s which does not exist", app.Spec.Project) } } - permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: newObj.GroupVersionKind().Group, Kind: newObj.GroupVersionKind().Kind}, newObj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { + permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { clusters, err := s.db.GetProjectClusters(context.TODO(), project) if err != nil { return nil, fmt.Errorf("failed to get project clusters: %w", err) @@ -2153,14 +2163,18 @@ func (s *Server) createResource(ctx context.Context, config *rest.Config, app *a return clusters, nil }) if err != nil { - return nil, fmt.Errorf("error checking resource permissions: %w", err) + return fmt.Errorf("error checking resource permissions: %w", err) } if !permitted { - return nil, fmt.Errorf("%s named %s creation not permitted in project: %s", newObj.GetKind(), newObj.GetName(), proj.Name) + return fmt.Errorf("%s named %s creation not permitted in project: %s", obj.GetKind(), obj.GetName(), proj.Name) } + return nil +} + +func (s *Server) createResource(ctx context.Context, config *rest.Config, app *appv1.Application, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { // Create the resource - _, err = s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + _, err := s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) if err != nil { return nil, fmt.Errorf("error creating resource: %w", err) } diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index 245b0de3bd98a..7b8b931da2132 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -970,11 +970,11 @@ definitions: job.spec.template = {} job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) - impactedResource123 = {} - impactedResource123.operation = "create" - impactedResource123.resource = job + impactedResource = {} + impactedResource.operation = "create" + impactedResource.resource = job result = {} - result[1] = impactedResource123 + result[1] = impactedResource return result` @@ -1026,6 +1026,112 @@ func TestNewStyleResourceActionPermitted(t *testing.T) { }) } +const newStyleActionsConfigMixedOk = `discovery.lua: return { sample = {} } +definitions: +- name: sample + action.lua: | + local os = require("os") + + function deepCopy(object) + local lookup_table = {} + local function _copy(obj) + if type(obj) ~= "table" then + return obj + elseif lookup_table[obj] then + return lookup_table[obj] + elseif next(obj) == nil then + return nil + else + local new_table = {} + lookup_table[obj] = new_table + for key, value in pairs(obj) do + new_table[_copy(key)] = _copy(value) + end + return setmetatable(new_table, getmetatable(obj)) + end + end + return _copy(object) + end + + job = {} + job.apiVersion = "batch/v1" + job.kind = "Job" + + job.metadata = {} + job.metadata.name = obj.metadata.name .. "-123" + job.metadata.namespace = obj.metadata.namespace + + ownerRef = {} + ownerRef.apiVersion = obj.apiVersion + ownerRef.kind = obj.kind + ownerRef.name = obj.metadata.name + ownerRef.uid = obj.metadata.uid + job.metadata.ownerReferences = {} + job.metadata.ownerReferences[1] = ownerRef + + job.spec = {} + job.spec.suspend = false + job.spec.template = {} + job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) + + impactedResource = {} + impactedResource.operation = "create" + impactedResource.resource = job + result = {} + result[1] = impactedResource + + obj.metadata.labels + + return result` + +func TestNewStyleResourceActionMixedOk(t *testing.T) { + Given(t). + Path(resourceActions). + ResourceOverrides(map[string]ResourceOverride{"batch/CronJob": {Actions: newStyleActionsConfigMixedOk}}). + ProjectSpec(AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []ApplicationDestination{{Namespace: "*", Server: "*"}}, + NamespaceResourceWhitelist: []metav1.GroupKind{ + {Group: "batch", Kind: "Job"}, + {Group: "batch", Kind: "CronJob"}, + {Group: "", Kind: "Pod"}, + }}). + When(). + CreateApp(). + Sync(). + Then(). + And(func(app *Application) { + + closer, client, err := ArgoCDClientset.NewApplicationClient() + assert.NoError(t, err) + defer io.Close(closer) + + actions, err := client.ListResourceActions(context.Background(), &applicationpkg.ApplicationResourceRequest{ + Name: &app.Name, + Group: pointer.String("batch"), + Kind: pointer.String("CronJob"), + Version: pointer.String("v1"), + Namespace: pointer.String(DeploymentNamespace()), + ResourceName: pointer.String("hello"), + }) + assert.NoError(t, err) + assert.Equal(t, []*ResourceAction{{Name: "sample", Disabled: false}}, actions.Actions) + + _, err = client.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{Name: &app.Name, + Group: pointer.String("batch"), + Kind: pointer.String("CronJob"), + Version: pointer.String("v1"), + Namespace: pointer.String(DeploymentNamespace()), + ResourceName: pointer.String("hello"), + Action: pointer.String("sample"), + }) + assert.NoError(t, err) + + _, err = KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-123", metav1.GetOptions{}) + assert.NoError(t, err) + }) +} + func TestSyncResourceByLabel(t *testing.T) { Given(t). Path(guestbookPath). diff --git a/util/lua/lua.go b/util/lua/lua.go index 73f0c30483d82..7ebfdae76151e 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/argoproj/gitops-engine/pkg/health" @@ -179,6 +180,14 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string if err != nil { return nil, err } + + // Make sure all the operations are supported + for _, impactedResource := range impactedResources { + supportedOperations := []string{"create", "patch"} + if !strings.Contains(strings.Join(supportedOperations, ","), impactedResource.K8SOperation) { + return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) + } + } } else { // The string represents an old-style action object output newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes)) diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index 27f35f480c468..570ad95570c9f 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -431,6 +431,25 @@ const expectedCreatedMultipleJobsObjList = ` namespace: test-ns ` +const expectedActionMixedOperationObjList = ` +- operation: create + resource: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-1 + namespace: test-ns +- operation: patch + resource: + apiVersion: batch/v1 + kind: CronJob + metadata: + name: hello + namespace: test-ns + labels: + test: test +` + const createJobActionLua = ` job = {} job.apiVersion = "batch/v1" @@ -478,6 +497,59 @@ impactedResource2.resource = job2 result[2] = impactedResource2 +return result +` +const mixedOperationActionLuaOk = ` +job1 = {} +job1.apiVersion = "batch/v1" +job1.kind = "Job" + +job1.metadata = {} +job1.metadata.name = "hello-1" +job1.metadata.namespace = obj.metadata.namespace + +impactedResource1 = {} +impactedResource1.operation = "create" +impactedResource1.resource = job1 +result = {} +result[1] = impactedResource1 + +obj.metadata.labels = {} +obj.metadata.labels["test"] = "test" + +impactedResource2 = {} +impactedResource2.operation = "patch" +impactedResource2.resource = obj + +result[2] = impactedResource2 + +return result +` + +const createMixedOperationActionLuaFailing = ` +job1 = {} +job1.apiVersion = "batch/v1" +job1.kind = "Job" + +job1.metadata = {} +job1.metadata.name = "hello-1" +job1.metadata.namespace = obj.metadata.namespace + +impactedResource1 = {} +impactedResource1.operation = "create" +impactedResource1.resource = job1 +result = {} +result[1] = impactedResource1 + +obj.metadata.labels = {} +obj.metadata.labels["test"] = "test" + +impactedResource2 = {} +impactedResource2.operation = "thisShouldFail" +impactedResource2.resource = obj + +result[2] = impactedResource2 + return result ` @@ -507,6 +579,27 @@ func TestExecuteNewStyleCreateActionMultipleResources(t *testing.T) { assert.Equal(t, expectedObjects, newObjects) } +func TestExecuteNewStyleActionMixedOperationsOk(t *testing.T) { + testObj := StrToUnstructured(cronJobObjYaml) + jsonBytes, err := yaml.YAMLToJSON([]byte(expectedActionMixedOperationObjList)) + assert.Nil(t, err) + // t.Log(bytes.NewBuffer(jsonBytes).String()) + expectedObjects, err := UnmarshalToImpactedResources(bytes.NewBuffer(jsonBytes).String()) + assert.Nil(t, err) + vm := VM{} + newObjects, err := vm.ExecuteResourceAction(testObj, mixedOperationActionLuaOk) + assert.Nil(t, err) + assert.Equal(t, expectedObjects, newObjects) +} + +func TestExecuteNewStyleActionMixedOperationsFailure(t *testing.T) { + testObj := StrToUnstructured(cronJobObjYaml) + vm := VM{} + _, err := vm.ExecuteResourceAction(testObj, createMixedOperationActionLuaFailing) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "unsupported operation") +} + func TestExecuteResourceActionNonTableReturn(t *testing.T) { testObj := StrToUnstructured(objJSON) vm := VM{} From 2a0b3c77ddf70d27cb2694433ee887df247926c1 Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 24 Mar 2023 23:08:30 +0300 Subject: [PATCH 23/38] upstream sync Signed-off-by: reggie --- server/application/application.go | 7 +- server/application/application_test.go | 228 +++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/server/application/application.go b/server/application/application.go index 3a47c7a419736..84aadf4fd931e 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2066,11 +2066,10 @@ func (s *Server) getAvailableActions(resourceOverrides map[string]appv1.Resource } func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceActionRunRequest) (*application.ApplicationResponse, error) { - appName := q.GetName() - appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) - app, err := s.appLister.Applications(appNs).Get(appName) + app, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionAction, q.GetAppNamespace(), q.GetName()) + // app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionAction, q.GetAppNamespace(), q.GetName(), "") if err != nil { - return nil, fmt.Errorf("error getting application: %w", err) + return nil, err } resourceRequest := &application.ApplicationResourceRequest{ Name: q.Name, diff --git a/server/application/application_test.go b/server/application/application_test.go index ec6ba93c11ed6..9cc56d651054f 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -24,6 +24,8 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" k8sappsv1 "k8s.io/api/apps/v1" + k8sbatchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -1688,3 +1690,229 @@ func TestInferResourcesStatusHealth(t *testing.T) { assert.Equal(t, health.HealthStatusDegraded, testApp.Status.Resources[0].Health.Status) assert.Nil(t, testApp.Status.Resources[1].Health) } + +func returnCronJob() *unstructured.Unstructured { + return &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "batch/v1", + "kind": "CronJob", + "metadata": map[string]interface{}{ + "name": "my-cron-job", + "namespace": testNamespace, + }, + "spec": map[string]interface{}{ + "jobTemplate": map[string]interface{}{ + "spec": map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "containers": []map[string]interface{}{{ + "name": "hello", + "image": "busybox:1.28", + "imagePullPolicy": "IfNotPresent", + "command": []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}}}, + // "resources": {}, + "restartPolicy": "OnFailure", + }, + }, + }, + }, + "schedule": "* * * * *", + }, + }} +} + +func TestRunNewStyleResourceAction(t *testing.T) { + cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) + + group := "batch" + kind := "CronJob" + version := "v1" + resourceName := "my-cron-job" + namespace := testNamespace + action := "create-job" + uid := "1" + + resources := []appsv1.ResourceStatus{{ + Group: group, + Kind: kind, + Name: resourceName, + Namespace: testNamespace, + Version: version, + }} + + appStateCache := appstate.NewCache(cacheClient, time.Minute) + + nodes := []appsv1.ResourceNode{{ + ResourceRef: appsv1.ResourceRef{ + Group: group, + Kind: kind, + Version: version, + Name: resourceName, + Namespace: testNamespace, + UID: uid, + }, + }} + + createJobDenyingProj := &appsv1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "createJobDenyingProj", Namespace: "default"}, + Spec: appsv1.AppProjectSpec{ + SourceRepos: []string{"*"}, + Destinations: []appsv1.ApplicationDestination{{Server: "*", Namespace: "*"}}, + NamespaceResourceWhitelist: []metav1.GroupKind{{Group: "never", Kind: "mind"}}, + }, + } + + cronJob := k8sbatchv1.CronJob{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "batch/v1", + Kind: "CronJob", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cron-job", + Namespace: testNamespace, + }, + Spec: k8sbatchv1.CronJobSpec{ + Schedule: "* * * * *", + JobTemplate: k8sbatchv1.JobTemplateSpec{ + Spec: k8sbatchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "hello", + Image: "busybox:1.28", + ImagePullPolicy: "IfNotPresent", + Command: []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}, + }, + }, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + }, + }, + } + + t.Run("CreateOperationNotPermitted", func(t *testing.T) { + testApp := newTestApp() + testApp.Spec.Project = "createJobDenyingProj" + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources + + appServer := newTestAppServer(t, testApp, createJobDenyingProj, kube.MustToUnstructured(&cronJob)) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) + + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) + + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + assert.Contains(t, runErr.Error(), "permission denied") + assert.Nil(t, appResponse) + }) + + t.Run("CreateOperationPermitted", func(t *testing.T) { + testApp := newTestApp() + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources + + appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&cronJob)) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) + + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) + + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + require.NoError(t, runErr) + assert.NotNil(t, appResponse) + }) +} + +func TestRunOldStyleResourceAction(t *testing.T) { + cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) + + group := "apps" + kind := "Deployment" + version := "v1" + resourceName := "nginx-deploy" + namespace := testNamespace + action := "pause" + uid := "2" + + resources := []appsv1.ResourceStatus{{ + Group: group, + Kind: kind, + Name: resourceName, + Namespace: testNamespace, + Version: version, + }} + + appStateCache := appstate.NewCache(cacheClient, time.Minute) + + nodes := []appsv1.ResourceNode{{ + ResourceRef: appsv1.ResourceRef{ + Group: group, + Kind: kind, + Version: version, + Name: resourceName, + Namespace: testNamespace, + UID: uid, + }, + }} + + deployment := k8sappsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-deploy", + Namespace: testNamespace, + }, + } + + t.Run("DefaultPatchOperation", func(t *testing.T) { + testApp := newTestApp() + testApp.Status.ResourceHealthSource = appsv1.ResourceHealthLocationAppTree + testApp.Status.Resources = resources + + // appServer := newTestAppServer(t, testApp, returnDeployment()) + appServer := newTestAppServer(t, testApp, kube.MustToUnstructured(&deployment)) + appServer.cache = servercache.NewCache(appStateCache, time.Minute, time.Minute, time.Minute) + + err := appStateCache.SetAppResourcesTree(testApp.Name, &appsv1.ApplicationTree{Nodes: nodes}) + require.NoError(t, err) + + appResponse, runErr := appServer.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{ + Name: &testApp.Name, + Namespace: &namespace, + Action: &action, + AppNamespace: &testApp.Namespace, + ResourceName: &resourceName, + Version: &version, + Group: &group, + Kind: &kind, + }) + + require.NoError(t, runErr) + assert.NotNil(t, appResponse) + }) +} From e99e0971ef33840c76f354a3dc5f926dbe83921b Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 25 Mar 2023 15:50:19 +0300 Subject: [PATCH 24/38] fix wrong return upon going over the impacted resources + docs + fixing e2e tests Signed-off-by: reggie --- docs/operator-manual/resource_actions.md | 24 +++++++++-------- server/application/application.go | 18 +++++++------ server/application/application_test.go | 34 +++--------------------- test/e2e/app_management_test.go | 21 ++++++++++----- util/lua/lua.go | 8 +++--- 5 files changed, 44 insertions(+), 61 deletions(-) diff --git a/docs/operator-manual/resource_actions.md b/docs/operator-manual/resource_actions.md index 46f9542aaf0c2..d156a124ab43c 100644 --- a/docs/operator-manual/resource_actions.md +++ b/docs/operator-manual/resource_actions.md @@ -71,8 +71,10 @@ Each action name must be represented in the list of `definitions` with an accomp #### Creating new resources with a custom action The resource the action is invoked on would be referred to as the `source resource`. -The new resource must be permitted on the AppProject level, otherwise the creation will fail. -If the new resource represents a k8s child of the source resource, the ownerReference should be set on the new resource. +The new resource and all the resources implicitly created as a result, must be permitted on the AppProject level, otherwise the creation will fail. + +##### Creating a source resource child resources with a custom action +If the new resource represents a k8s child of the source resource, the source resource ownerReference must be set on the new resource. Here is an example Lua snippet, that takes care of constructing a Job resource that is a child of a source CronJob resource - the `obj` is a global variable, which contains the source resource: ``` ... @@ -87,7 +89,7 @@ job.metadata.ownerReferences = {} job.metadata.ownerReferences[1] = ownerRef ... ``` - +##### Creating independent child resources with a custom action If the new resource is independent of the source resource, the default behavior of such new resource is that it is not known by the App of the source resource (as it is not part of the desired state and does not have an `ownerReference`). To make the App aware of the new resource, the `app.kubernetes.io/instance` label (or other ArgoCD tracking label, if configured) must be set on the resource. It can be copied from the source resource, like this: ``` @@ -98,14 +100,14 @@ newObj.metadata.labels = {} newObj.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] ... ``` -While the new resource will be part of the App with the tracking label in place, it will be immediately pruned if auto prune is set on the App. To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: +While the new resource will be part of the App with the tracking label in place, it will be immediately deleted if auto prune is set on the App. To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: ``` ... newObj.metadata.annotations = {} newObj.metadata.annotations["argocd.argoproj.io/sync-options"] = "Prune=false" ... ``` -(If setting `Prune=false` behavior, the resource will not be deleted upon the deletion of the App, and will require a manual cleanup.) +(If setting `Prune=false` behavior, the resource will not be deleted upon the deletion of the App, and will require a manual cleanup). The resource and the App will now appear out of sync - which is the expected ArgoCD behavior upon creating a resource that is not part of the desired state. @@ -126,7 +128,7 @@ resource.customizations.actions.ConfigMap: | definitions: - name: do-things action.lua: | - # Create a new ConfigMap + -- Create a new ConfigMap cm1 = {} cm1.apiVersion = "v1" cm1.kind = "ConfigMap" @@ -134,12 +136,12 @@ resource.customizations.actions.ConfigMap: | cm1.metadata.name = "cm1" cm1.metadata.namespace = obj.metadata.namespace cm1.metadata.labels = {} - # Copy ArgoCD tracking label so that the resource is recognized by the App + -- Copy ArgoCD tracking label so that the resource is recognized by the App cm1.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] cm1.metadata.annotations = {} - # For Apps with auto-prune, set the prune false on the resource, so it does not get deleted + -- For Apps with auto-prune, set the prune false on the resource, so it does not get deleted cm1.metadata.annotations["argocd.argoproj.io/sync-options"] = "Prune=false" - # Keep the App synced even though it has a resource that is not in Git + -- Keep the App synced even though it has a resource that is not in Git cm1.metadata.annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" cm1.data = {} cm1.data.myKey1 = "myValue1" @@ -147,8 +149,8 @@ resource.customizations.actions.ConfigMap: | impactedResource1.operation = "create" impactedResource1.resource = cm1 - # Patch the original cm - obj.metadata.label["aKey"] = "aValue" + -- Patch the original cm + obj.metadata.labels["aKey"] = "aValue" impactedResource2 = {} impactedResource2.operation = "patch" impactedResource2.resource = obj diff --git a/server/application/application.go b/server/application/application.go index 84aadf4fd931e..72d9aca1ad17f 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2066,11 +2066,13 @@ func (s *Server) getAvailableActions(resourceOverrides map[string]appv1.Resource } func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceActionRunRequest) (*application.ApplicationResponse, error) { - app, err := s.getApplicationEnforceRBACInformer(ctx, rbacpolicy.ActionAction, q.GetAppNamespace(), q.GetName()) - // app, err := s.getApplicationEnforceRBACClient(ctx, rbacpolicy.ActionAction, q.GetAppNamespace(), q.GetName(), "") + appName := q.GetName() + appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) + app, err := s.appLister.Applications(appNs).Get(appName) if err != nil { - return nil, err + return nil, permissionDeniedErr } + resourceRequest := &application.ApplicationResourceRequest{ Name: q.Name, AppNamespace: q.AppNamespace, @@ -2122,17 +2124,17 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj newObjBytes, err := json.Marshal(newObj) + if err != nil { return nil, fmt.Errorf("error marshaling new object: %w", err) } switch impactedResource.K8SOperation { + // No default case since a not supported operation would have failed upon luaVM.ExecuteResourceAction case "patch": - return s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) + s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) case "create": - return s.createResource(ctx, config, app, newObj) - - // No default case since a not supported operation fails upon luaVM.ExecuteResourceAction + s.createResource(ctx, config, app, newObj) } } @@ -2206,7 +2208,7 @@ func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Applica return fmt.Errorf("error checking resource permissions: %w", err) } if !permitted { - return fmt.Errorf("%s named %s creation not permitted in project: %s", obj.GetKind(), obj.GetName(), proj.Name) + return permissionDeniedErr } return nil diff --git a/server/application/application_test.go b/server/application/application_test.go index 9cc56d651054f..1fe8112e28367 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -1691,35 +1691,6 @@ func TestInferResourcesStatusHealth(t *testing.T) { assert.Nil(t, testApp.Status.Resources[1].Health) } -func returnCronJob() *unstructured.Unstructured { - return &unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": "batch/v1", - "kind": "CronJob", - "metadata": map[string]interface{}{ - "name": "my-cron-job", - "namespace": testNamespace, - }, - "spec": map[string]interface{}{ - "jobTemplate": map[string]interface{}{ - "spec": map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "containers": []map[string]interface{}{{ - "name": "hello", - "image": "busybox:1.28", - "imagePullPolicy": "IfNotPresent", - "command": []string{"/bin/sh", "-c", "date; echo Hello from the Kubernetes cluster"}}}, - // "resources": {}, - "restartPolicy": "OnFailure", - }, - }, - }, - }, - "schedule": "* * * * *", - }, - }} -} - func TestRunNewStyleResourceAction(t *testing.T) { cacheClient := cacheutil.NewCache(cacheutil.NewInMemoryCache(1 * time.Hour)) @@ -1769,6 +1740,9 @@ func TestRunNewStyleResourceAction(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "my-cron-job", Namespace: testNamespace, + Labels: map[string]string{ + "some": "label", + }, }, Spec: k8sbatchv1.CronJobSpec{ Schedule: "* * * * *", @@ -1815,7 +1789,7 @@ func TestRunNewStyleResourceAction(t *testing.T) { Kind: &kind, }) - assert.Contains(t, runErr.Error(), "permission denied") + assert.Equal(t, permissionDeniedErr.Error(), runErr.Error()) assert.Nil(t, appResponse) }) diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index cf26456f35428..01a4b1ce16d01 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -992,7 +992,6 @@ func TestNewStyleResourceActionPermitted(t *testing.T) { NamespaceResourceWhitelist: []metav1.GroupKind{ {Group: "batch", Kind: "Job"}, {Group: "batch", Kind: "CronJob"}, - {Group: "", Kind: "Pod"}, }}). When(). CreateApp(). @@ -1078,13 +1077,18 @@ definitions: job.spec.template = {} job.spec.template.spec = deepCopy(obj.spec.jobTemplate.spec.template.spec) - impactedResource = {} - impactedResource.operation = "create" - impactedResource.resource = job + impactedResource1 = {} + impactedResource1.operation = "create" + impactedResource1.resource = job result = {} - result[1] = impactedResource + result[1] = impactedResource1 + + obj.metadata.labels["aKey"] = 'aValue' + impactedResource2 = {} + impactedResource2.operation = "patch" + impactedResource2.resource = obj - obj.metadata.labels + result[2] = impactedResource2 return result` @@ -1098,7 +1102,6 @@ func TestNewStyleResourceActionMixedOk(t *testing.T) { NamespaceResourceWhitelist: []metav1.GroupKind{ {Group: "batch", Kind: "Job"}, {Group: "batch", Kind: "CronJob"}, - {Group: "", Kind: "Pod"}, }}). When(). CreateApp(). @@ -1131,7 +1134,11 @@ func TestNewStyleResourceActionMixedOk(t *testing.T) { }) assert.NoError(t, err) + // Assert new Job was created _, err = KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-123", metav1.GetOptions{}) + // Assert the original CronJob was patched + cronJob, err := KubeClientset.BatchV1().CronJobs(DeploymentNamespace()).Get(context.Background(), "hello", metav1.GetOptions{}) + assert.Equal(t, "aValue", cronJob.Labels["aKey"]) assert.NoError(t, err) }) } diff --git a/util/lua/lua.go b/util/lua/lua.go index 7ebfdae76151e..1ee3152094c6f 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -166,15 +166,13 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string var impactedResources []ImpactedResource jsonString := bytes.NewBuffer(jsonBytes).String() - if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { - if len(jsonString) < 2 { - return nil, fmt.Errorf("Lua output was not a valid json object or array") - } - } // The output from Lua is either an object (old-style action output) or an array (new-style action output). // Check whether the string starts with an opening square bracket and ends with a closing square bracket, // avoiding programming by exception. if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { + if len(jsonString) < 2 { + return nil, fmt.Errorf("Lua output was not a valid json object or array") + } // The string represents a new-style action array output impactedResources, err = UnmarshalToImpactedResources(string(jsonBytes)) if err != nil { From 3d271c0097910675ceef42422e8a389d2d2486d2 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 25 Mar 2023 15:54:44 +0300 Subject: [PATCH 25/38] docs Signed-off-by: reggie --- docs/operator-manual/resource_actions.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/operator-manual/resource_actions.md b/docs/operator-manual/resource_actions.md index d156a124ab43c..8f3862450e434 100644 --- a/docs/operator-manual/resource_actions.md +++ b/docs/operator-manual/resource_actions.md @@ -23,11 +23,11 @@ This action modifies and returns the source resource. This kind of action was the only one available till 2.8, and it is still supported. #### An action that produces a list of new or modified resources -An alpha feature, introduced in 2.8. -This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to perform on. -Currently supported operations are "create" and "patch", "patch" is only supported for the source resource. -Creating new resources is possible, by specifying a "create" operation for each such resource in the returned list. -One of the returned resources can be the modified source object, with a "patch" operation, if needed. +An alpha feature, introduced in 2.8. +This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to perform on. +Currently supported operations are "create" and "patch", "patch" is only supported for the source resource. +Creating new resources is possible, by specifying a "create" operation for each such resource in the returned list. +One of the returned resources can be the modified source object, with a "patch" operation, if needed. See the definition examples below. ### Define a Custom Resource Action in `argocd-cm` ConfigMap @@ -91,7 +91,8 @@ job.metadata.ownerReferences[1] = ownerRef ``` ##### Creating independent child resources with a custom action If the new resource is independent of the source resource, the default behavior of such new resource is that it is not known by the App of the source resource (as it is not part of the desired state and does not have an `ownerReference`). -To make the App aware of the new resource, the `app.kubernetes.io/instance` label (or other ArgoCD tracking label, if configured) must be set on the resource. It can be copied from the source resource, like this: +To make the App aware of the new resource, the `app.kubernetes.io/instance` label (or other ArgoCD tracking label, if configured) must be set on the resource. +It can be copied from the source resource, like this: ``` ... newObj = {} @@ -100,7 +101,8 @@ newObj.metadata.labels = {} newObj.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] ... ``` -While the new resource will be part of the App with the tracking label in place, it will be immediately deleted if auto prune is set on the App. To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: +While the new resource will be part of the App with the tracking label in place, it will be immediately deleted if auto prune is set on the App. +To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: ``` ... newObj.metadata.annotations = {} From 6df8f230868de66e1e609a4546661e227a451861 Mon Sep 17 00:00:00 2001 From: reggie Date: Sat, 25 Mar 2023 16:30:37 +0300 Subject: [PATCH 26/38] better error handling Signed-off-by: reggie --- server/application/application.go | 10 ++++++++-- test/e2e/app_management_test.go | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/application/application.go b/server/application/application.go index 72d9aca1ad17f..c3af5d67904df 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2132,9 +2132,15 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA switch impactedResource.K8SOperation { // No default case since a not supported operation would have failed upon luaVM.ExecuteResourceAction case "patch": - s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) + _, err := s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) + if err != nil { + return nil, err + } case "create": - s.createResource(ctx, config, app, newObj) + _, err := s.createResource(ctx, config, app, newObj) + if err != nil { + return nil, err + } } } diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index 01a4b1ce16d01..85161971aec8f 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -1136,6 +1136,7 @@ func TestNewStyleResourceActionMixedOk(t *testing.T) { // Assert new Job was created _, err = KubeClientset.BatchV1().Jobs(DeploymentNamespace()).Get(context.Background(), "hello-123", metav1.GetOptions{}) + assert.NoError(t, err) // Assert the original CronJob was patched cronJob, err := KubeClientset.BatchV1().CronJobs(DeploymentNamespace()).Get(context.Background(), "hello", metav1.GetOptions{}) assert.Equal(t, "aValue", cronJob.Labels["aKey"]) From 58893d434e4de2db1f7e06cac53a0901d56dfdb3 Mon Sep 17 00:00:00 2001 From: reggie Date: Thu, 13 Apr 2023 19:45:48 +0300 Subject: [PATCH 27/38] K8SOperation as an enum Signed-off-by: reggie --- cmd/argocd/commands/admin/settings.go | 3 +-- server/application/application.go | 2 +- util/lua/custom_actions_test.go | 3 +-- util/lua/impacted_resource.go | 35 +++++++++++++++++++++++++-- util/lua/lua.go | 9 ------- util/lua/lua_test.go | 4 +-- 6 files changed, 38 insertions(+), 18 deletions(-) diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index b80b32e439c03..0f57b86180c31 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -548,6 +548,7 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a for _, impactedResource := range modifiedRes { result := impactedResource.UnstructuredObj switch impactedResource.K8SOperation { + // No default case since a not supported operation would have failed upon unmarshaling earlier case "patch": if reflect.DeepEqual(&res, modifiedRes) { _, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name) @@ -561,8 +562,6 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a errors.CheckError(err) fmt.Println("Following resource was created:") fmt.Println(bytes.NewBuffer(yamlBytes).String()) - default: - errors.CheckError(fmt.Errorf("Unsupported operation: %s", impactedResource.K8SOperation)) } } diff --git a/server/application/application.go b/server/application/application.go index c3af5d67904df..6e67c2c604b42 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2130,7 +2130,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA } switch impactedResource.K8SOperation { - // No default case since a not supported operation would have failed upon luaVM.ExecuteResourceAction + // No default case since a not supported operation would have failed upon unmarshaling earlier case "patch": _, err := s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) if err != nil { diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 9feca174afcc7..3c5e5afbeb176 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -150,6 +150,7 @@ func TestLuaResourceActionsScript(t *testing.T) { assert.NotNil(t, expectedObj) switch impactedResource.K8SOperation { + // No default case since a not supported operation would have failed upon unmarshaling earlier case "patch": // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource assert.EqualValues(t, sourceObj.GroupVersionKind(), result.GroupVersionKind()) @@ -157,8 +158,6 @@ func TestLuaResourceActionsScript(t *testing.T) { assert.EqualValues(t, sourceObj.GetNamespace(), result.GetNamespace()) case "create": // no special logic to test for now - default: - t.Error("Operation not supported: " + impactedResource.K8SOperation) } // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) diff --git a/util/lua/impacted_resource.go b/util/lua/impacted_resource.go index 63fc0165523be..191b3f68b9b61 100644 --- a/util/lua/impacted_resource.go +++ b/util/lua/impacted_resource.go @@ -1,6 +1,8 @@ package lua import ( + "fmt" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -10,8 +12,37 @@ import ( // This replaces the traditional architecture of "Lua action returns the source resource for ArgoCD to patch". // This enables ArgoCD to create NEW resources upon custom action. // Note that the Lua code in the custom action is coupled to this type, since Lua json output is then unmarshalled to this struct. -// TODO: maybe K8SOperation needs to be an enum of supported k8s verbs, with a custom json marshaller/unmarshaller +type K8SOperation string + +const ( + CreateOperation K8SOperation = "create" + PatchOperation K8SOperation = "patch" +) + type ImpactedResource struct { UnstructuredObj *unstructured.Unstructured `json:"resource"` - K8SOperation string `json:"operation"` + K8SOperation K8SOperation `json:"operation"` +} + +func (op *K8SOperation) UnmarshalJSON(data []byte) error { + switch string(data) { + case `"create"`: + *op = CreateOperation + case `"patch"`: + *op = PatchOperation + default: + return fmt.Errorf("unsupported operation: %s", data) + } + return nil +} + +func (op K8SOperation) MarshalJSON() ([]byte, error) { + switch op { + case CreateOperation: + return []byte(`"create"`), nil + case PatchOperation: + return []byte(`"patch"`), nil + default: + return nil, fmt.Errorf("unsupported operation: %s", op) + } } diff --git a/util/lua/lua.go b/util/lua/lua.go index 1ee3152094c6f..f237e4f348a05 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -7,7 +7,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "time" "github.com/argoproj/gitops-engine/pkg/health" @@ -178,14 +177,6 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string if err != nil { return nil, err } - - // Make sure all the operations are supported - for _, impactedResource := range impactedResources { - supportedOperations := []string{"create", "patch"} - if !strings.Contains(strings.Join(supportedOperations, ","), impactedResource.K8SOperation) { - return nil, fmt.Errorf("unsupported operation: %s", impactedResource.K8SOperation) - } - } } else { // The string represents an old-style action object output newObj, err := appv1.UnmarshalToUnstructured(string(jsonBytes)) diff --git a/util/lua/lua_test.go b/util/lua/lua_test.go index 570ad95570c9f..e909417c2aa1c 100644 --- a/util/lua/lua_test.go +++ b/util/lua/lua_test.go @@ -392,7 +392,7 @@ func TestExecuteOldStyleResourceAction(t *testing.T) { newObjects, err := vm.ExecuteResourceAction(testObj, validActionLua) assert.Nil(t, err) assert.Equal(t, len(newObjects), 1) - assert.Equal(t, newObjects[0].K8SOperation, "patch") + assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch")) assert.Equal(t, expectedLuaUpdatedObj, newObjects[0].UnstructuredObj) } @@ -673,7 +673,7 @@ func TestCleanPatch(t *testing.T) { newObjects, err := vm.ExecuteResourceAction(testObj, pausedToFalseLua) assert.Nil(t, err) assert.Equal(t, len(newObjects), 1) - assert.Equal(t, newObjects[0].K8SOperation, "patch") + assert.Equal(t, newObjects[0].K8SOperation, K8SOperation("patch")) assert.Equal(t, expectedObj, newObjects[0].UnstructuredObj) } From 8dabaf3a7b7be660580f39360fd426336b2b4e8c Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 14 Apr 2023 14:04:37 +0300 Subject: [PATCH 28/38] added dry-run for create operation Signed-off-by: reggie --- cmd/argocd/commands/admin/settings.go | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- server/application/application.go | 26 +++++++++++++++++++++----- util/lua/custom_actions_test.go | 4 ++-- util/lua/impacted_resource.go | 6 ++++-- util/lua/lua.go | 4 ++-- 7 files changed, 34 insertions(+), 16 deletions(-) diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index 0f57b86180c31..e2992f54e0180 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -549,7 +549,7 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a result := impactedResource.UnstructuredObj switch impactedResource.K8SOperation { // No default case since a not supported operation would have failed upon unmarshaling earlier - case "patch": + case lua.PatchOperation: if reflect.DeepEqual(&res, modifiedRes) { _, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name) return @@ -557,7 +557,7 @@ argocd admin settings resource-overrides action run /tmp/deploy.yaml restart --a _, _ = fmt.Printf("Following fields have been changed:\n\n") _ = cli.PrintDiff(res.GetName(), &res, result) - case "create": + case lua.CreateOperation: yamlBytes, err := yaml.Marshal(impactedResource.UnstructuredObj) errors.CheckError(err) fmt.Println("Following resource was created:") diff --git a/go.mod b/go.mod index 7d76c2352bfe4..f679c95d84bc0 100644 --- a/go.mod +++ b/go.mod @@ -299,4 +299,4 @@ replace ( k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) -replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 +replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230413172702-9601b03cb268 diff --git a/go.sum b/go.sum index 375815f2169e8..ba0639052427a 100644 --- a/go.sum +++ b/go.sum @@ -913,8 +913,8 @@ github.com/r3labs/diff v1.1.0/go.mod h1:7WjXasNzi0vJetRcB/RqNl5dlIsmXcTTLmF5IoH6 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= -github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390 h1:47IKb6USJNlvb0Gf/m1cOTA0UCnaGOnQh91TI+ylQJk= -github.com/reggie-k/gitops-engine v0.0.0-20230313191414-ee2415c2f390/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/reggie-k/gitops-engine v0.0.0-20230413172702-9601b03cb268 h1:Yi4S4n446+drtWd7owYpp2gnGng+EsW8TezOpFFep7w= +github.com/reggie-k/gitops-engine v0.0.0-20230413172702-9601b03cb268/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/server/application/application.go b/server/application/application.go index 6e67c2c604b42..58e62cbc04c7a 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2111,16 +2111,32 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error executing Lua resource action: %w", err) } - // First, make sure all the returned resources are permitted, no matter for which operation. + // First, make sure all the returned resources are permitted, for each operation. + // Also perform create with dry-runs for all create-operation resources. + // This is performed separately to reduce the risk of only some of the resources being successfully created later. + // TODO: when apply/delete operations would be supported for custom actions, + // the dry-run for relevant apply/delete operation would have to be invoked as well. for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj err := s.verifyResourcePermitted(ctx, app, newObj) if err != nil { return nil, err } + switch impactedResource.K8SOperation { + case lua.CreateOperation: + createOptions := metav1.CreateOptions{DryRun: []string{"All"}} + _, err := s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj, createOptions) + if err != nil { + return nil, err + } + } } - // Now, perform the actual operations + // Now, perform the actual operations. + // The creation itself is not transactional. + // TODO: maybe create a k8s list representation of the resources, + // and invoke create on this list resource to make it semi-transactional (there is still patch operation that is separate, + // thus can fail separately from create). for _, impactedResource := range newObjects { newObj := impactedResource.UnstructuredObj newObjBytes, err := json.Marshal(newObj) @@ -2131,12 +2147,12 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA switch impactedResource.K8SOperation { // No default case since a not supported operation would have failed upon unmarshaling earlier - case "patch": + case lua.PatchOperation: _, err := s.patchResource(ctx, config, liveObjBytes, newObjBytes, newObj) if err != nil { return nil, err } - case "create": + case lua.CreateOperation: _, err := s.createResource(ctx, config, app, newObj) if err != nil { return nil, err @@ -2222,7 +2238,7 @@ func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Applica func (s *Server) createResource(ctx context.Context, config *rest.Config, app *appv1.Application, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { // Create the resource - _, err := s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj) + _, err := s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf("error creating resource: %w", err) } diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 3c5e5afbeb176..ab1f3bfeb6d6b 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -151,12 +151,12 @@ func TestLuaResourceActionsScript(t *testing.T) { switch impactedResource.K8SOperation { // No default case since a not supported operation would have failed upon unmarshaling earlier - case "patch": + case PatchOperation: // Patching is only allowed for the source resource, so the GVK + name + ns must be the same as the impacted resource assert.EqualValues(t, sourceObj.GroupVersionKind(), result.GroupVersionKind()) assert.EqualValues(t, sourceObj.GetName(), result.GetName()) assert.EqualValues(t, sourceObj.GetNamespace(), result.GetNamespace()) - case "create": + case CreateOperation: // no special logic to test for now } // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. diff --git a/util/lua/impacted_resource.go b/util/lua/impacted_resource.go index 191b3f68b9b61..de6ab5933372d 100644 --- a/util/lua/impacted_resource.go +++ b/util/lua/impacted_resource.go @@ -6,12 +6,14 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -// This struct represents a wrapper, that is returned from Lua custom action script, around the unstructured k8s resource + a k8s verb +// This struct represents a wrapper, that is returned from Lua custom action script, around the unstructured k8s resource + a k8s operation // that will need to be performed on this returned resource. -// Currently only "create" and "patch" operations are supported for custom actions +// Currently only "create" and "patch" operations are supported for custom actions. // This replaces the traditional architecture of "Lua action returns the source resource for ArgoCD to patch". // This enables ArgoCD to create NEW resources upon custom action. // Note that the Lua code in the custom action is coupled to this type, since Lua json output is then unmarshalled to this struct. +// Avoided using iota, since need the mapping of the string value the end users will write in Lua code ("create" and "patch"). +// TODO: maybe there is a nicer general way to marshal and unmarshal, instead of explicit iteration over the enum values. type K8SOperation string const ( diff --git a/util/lua/lua.go b/util/lua/lua.go index f237e4f348a05..a3915098d595d 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -185,12 +185,12 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string } // Wrap the old-style action output with a single-member array. // The default definition of the old-style action is a "patch" one. - impactedResources = append(impactedResources, ImpactedResource{newObj, "patch"}) + impactedResources = append(impactedResources, ImpactedResource{newObj, PatchOperation}) } for _, impactedResource := range impactedResources { // Cleaning the resource is only relevant to "patch" - if impactedResource.K8SOperation == "patch" { + if impactedResource.K8SOperation == PatchOperation { impactedResource.UnstructuredObj.Object = cleanReturnedObj(impactedResource.UnstructuredObj.Object, obj.Object) } From 73d2cdd7655476fdb2b580c6d94ea6000815f86f Mon Sep 17 00:00:00 2001 From: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Date: Sat, 27 May 2023 14:39:47 -0400 Subject: [PATCH 29/38] small changes Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- server/application/application.go | 25 ++++++++++++++----------- util/lua/lua.go | 6 +++--- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index f8a35b3c410fc..2ce1fa81bac76 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.23.1 github.com/antonmedv/expr v1.9.0 - github.com/argoproj/gitops-engine v0.7.1-0.20230512020822-b4dd8b8c3976 + github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d github.com/aws/aws-sdk-go v1.44.164 diff --git a/go.sum b/go.sum index 79c0571043f8e..e50dd08b2a861 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230512020822-b4dd8b8c3976 h1:8i12dOcimhwrJxUznzZR/NW4JpIL5DXZjkI3Bl3yh38= -github.com/argoproj/gitops-engine v0.7.1-0.20230512020822-b4dd8b8c3976/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 h1:txH/SJQJJTdqmKnbQ8sKYGAgjBy4VvWtS45trc5yW/0= +github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da h1:Vf9xvHcXn4TP/nLIfWn+TaC521V9fpz/DwRP6uEeVR8= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da/go.mod h1:05koR0gE/O0i5YDbidg1dpr76XitK4DJveh+dIAq6e8= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= diff --git a/server/application/application.go b/server/application/application.go index ad017c0979095..4c378d0a63458 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -2112,13 +2112,6 @@ func (s *Server) getAvailableActions(resourceOverrides map[string]appv1.Resource } func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceActionRunRequest) (*application.ApplicationResponse, error) { - appName := q.GetName() - appNs := s.appNamespaceOrDefault(q.GetAppNamespace()) - app, err := s.appLister.Applications(appNs).Get(appName) - if err != nil { - return nil, permissionDeniedErr - } - resourceRequest := &application.ApplicationResourceRequest{ Name: q.Name, AppNamespace: q.AppNamespace, @@ -2157,6 +2150,16 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, fmt.Errorf("error executing Lua resource action: %w", err) } + var app *appv1.Application + // Only bother getting the app if we know we're going to need it for a resource permission check. + if len(newObjects) > 0 { + // No need for an RBAC check, we checked above that the user is allowed to run this action. + app, err = s.appLister.Applications(s.appNamespaceOrDefault(q.GetAppNamespace())).Get(q.GetName()) + if err != nil { + return nil, err + } + } + // First, make sure all the returned resources are permitted, for each operation. // Also perform create with dry-runs for all create-operation resources. // This is performed separately to reduce the risk of only some of the resources being successfully created later. @@ -2199,7 +2202,7 @@ func (s *Server) RunResourceAction(ctx context.Context, q *application.ResourceA return nil, err } case lua.CreateOperation: - _, err := s.createResource(ctx, config, app, newObj) + _, err := s.createResource(ctx, config, newObj) if err != nil { return nil, err } @@ -2264,6 +2267,7 @@ func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Applica if apierr.IsNotFound(err) { return fmt.Errorf("application references project %s which does not exist", app.Spec.Project) } + return fmt.Errorf("failed to get project %s: %w", app.Spec.Project, err) } permitted, err := proj.IsResourcePermitted(schema.GroupKind{Group: obj.GroupVersionKind().Group, Kind: obj.GroupVersionKind().Kind}, obj.GetNamespace(), app.Spec.Destination, func(project string) ([]*appv1.Cluster, error) { clusters, err := s.db.GetProjectClusters(context.TODO(), project) @@ -2276,14 +2280,13 @@ func (s *Server) verifyResourcePermitted(ctx context.Context, app *appv1.Applica return fmt.Errorf("error checking resource permissions: %w", err) } if !permitted { - return permissionDeniedErr + return fmt.Errorf("application %s is not permitted to manage %s/%s/%s in %s", app.RBACName(s.ns), obj.GroupVersionKind().Group, obj.GroupVersionKind().Kind, obj.GetName(), obj.GetNamespace()) } return nil } -func (s *Server) createResource(ctx context.Context, config *rest.Config, app *appv1.Application, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { - // Create the resource +func (s *Server) createResource(ctx context.Context, config *rest.Config, newObj *unstructured.Unstructured) (*application.ApplicationResponse, error) { _, err := s.kubectl.CreateResource(ctx, config, newObj.GroupVersionKind(), newObj.GetName(), newObj.GetNamespace(), newObj, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf("error creating resource: %w", err) diff --git a/util/lua/lua.go b/util/lua/lua.go index a3915098d595d..d9849708e8c95 100644 --- a/util/lua/lua.go +++ b/util/lua/lua.go @@ -165,13 +165,13 @@ func (vm VM) ExecuteResourceAction(obj *unstructured.Unstructured, script string var impactedResources []ImpactedResource jsonString := bytes.NewBuffer(jsonBytes).String() + if len(jsonString) < 2 { + return nil, fmt.Errorf("Lua output was not a valid json object or array") + } // The output from Lua is either an object (old-style action output) or an array (new-style action output). // Check whether the string starts with an opening square bracket and ends with a closing square bracket, // avoiding programming by exception. if jsonString[0] == '[' && jsonString[len(jsonString)-1] == ']' { - if len(jsonString) < 2 { - return nil, fmt.Errorf("Lua output was not a valid json object or array") - } // The string represents a new-style action array output impactedResources, err = UnmarshalToImpactedResources(string(jsonBytes)) if err != nil { From b1e845bd3ac8979599879c03c8adb6929b4da032 Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 30 May 2023 16:21:22 +0300 Subject: [PATCH 30/38] ref to my gitops-engine fork out Signed-off-by: reggie --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 9aedecebcf180..08592afbab511 100644 --- a/go.mod +++ b/go.mod @@ -290,5 +290,3 @@ replace ( k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 ) - -replace github.com/argoproj/gitops-engine v0.7.1-0.20230214165351-ed70eac8b7bd => github.com/reggie-k/gitops-engine v0.0.0-20230413172702-9601b03cb268 From 872630c46bdba24d0a7494c48e0a319aa824ff7a Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 30 May 2023 16:39:57 +0300 Subject: [PATCH 31/38] ref to my gitops-engine fork out Signed-off-by: reggie --- go.mod | 6 ++++-- go.sum | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 08592afbab511..c981891528caf 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,8 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.23.1 github.com/antonmedv/expr v1.9.0 - github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 + // github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 + github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d github.com/aws/aws-sdk-go v1.44.271 @@ -23,7 +24,6 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible github.com/fsnotify/fsnotify v1.6.0 github.com/gfleury/go-bitbucket-v1 v0.0.0-20220301131131-8e7ed04b843e - github.com/ghodss/yaml v1.0.0 github.com/go-git/go-git/v5 v5.6.1 github.com/go-logr/logr v1.2.3 github.com/go-openapi/loads v0.21.2 @@ -104,6 +104,8 @@ require ( sigs.k8s.io/yaml v1.3.0 ) +require github.com/ghodss/yaml v1.0.0 + require ( cloud.google.com/go/compute v1.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect diff --git a/go.sum b/go.sum index 7ab719571990b..81ee867e64c12 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 h1:txH/SJQJJTdqmKnbQ8sKYGAgjBy4VvWtS45trc5yW/0= -github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc h1:i6OgOvFFsoWiGBFEhhDNcYoRtxxtrVwcD7wCEeqhct4= +github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da h1:Vf9xvHcXn4TP/nLIfWn+TaC521V9fpz/DwRP6uEeVR8= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da/go.mod h1:05koR0gE/O0i5YDbidg1dpr76XitK4DJveh+dIAq6e8= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= From 128d78be50c173706e73ccceef784ed29a217d76 Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 30 May 2023 16:41:40 +0300 Subject: [PATCH 32/38] ref to my gitops-engine fork out Signed-off-by: reggie --- go.mod | 6 ++---- go.sum | 14 +++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index c981891528caf..be9af515747a6 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.23.1 github.com/antonmedv/expr v1.9.0 - // github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d @@ -104,8 +103,6 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/ghodss/yaml v1.0.0 - require ( cloud.google.com/go/compute v1.7.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -140,6 +137,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect @@ -291,4 +289,4 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index 81ee867e64c12..7b801b1baa680 100644 --- a/go.sum +++ b/go.sum @@ -567,8 +567,9 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -737,8 +738,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -1048,8 +1049,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 h1:qqllXPzXh+So+mmANlX/gCJrgo+1kQyshMoQ+NASzm0= -github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= +github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= +github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw= github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -1451,7 +1452,6 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1911,4 +1911,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file From 89f2dcfe7a7dd5dbe208f80a62a701e3bdec63f0 Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 30 May 2023 18:27:31 +0300 Subject: [PATCH 33/38] ref to my gitops-engine fork out Signed-off-by: reggie --- go.mod | 4 ++-- go.sum | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index be9af515747a6..962c3aed89fc6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible github.com/fsnotify/fsnotify v1.6.0 github.com/gfleury/go-bitbucket-v1 v0.0.0-20220301131131-8e7ed04b843e + github.com/ghodss/yaml v1.0.0 github.com/go-git/go-git/v5 v5.6.1 github.com/go-logr/logr v1.2.3 github.com/go-openapi/loads v0.21.2 @@ -137,7 +138,6 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect @@ -289,4 +289,4 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 7b801b1baa680..81ee867e64c12 100644 --- a/go.sum +++ b/go.sum @@ -567,9 +567,8 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -738,8 +737,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -1049,8 +1048,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= -github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= +github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 h1:qqllXPzXh+So+mmANlX/gCJrgo+1kQyshMoQ+NASzm0= +github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= github.com/xanzy/go-gitlab v0.83.0 h1:37p0MpTPNbsTMKX/JnmJtY8Ch1sFiJzVF342+RvZEGw= github.com/xanzy/go-gitlab v0.83.0/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -1452,6 +1451,7 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1911,4 +1911,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= \ No newline at end of file +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= From c139cea98a86554d052776decd54f23d63a9ed3a Mon Sep 17 00:00:00 2001 From: reggie Date: Tue, 30 May 2023 18:29:15 +0300 Subject: [PATCH 34/38] ref to my gitops-engine fork out Signed-off-by: reggie --- go.mod | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 962c3aed89fc6..cc207f6e8b36b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/evanphx/json-patch v5.6.0+incompatible github.com/fsnotify/fsnotify v1.6.0 github.com/gfleury/go-bitbucket-v1 v0.0.0-20220301131131-8e7ed04b843e - github.com/ghodss/yaml v1.0.0 github.com/go-git/go-git/v5 v5.6.1 github.com/go-logr/logr v1.2.3 github.com/go-openapi/loads v0.21.2 @@ -41,7 +40,7 @@ require ( github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.0 github.com/gosimple/slug v1.13.1 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 @@ -49,11 +48,11 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.2 github.com/imdario/mergo v0.3.13 github.com/improbable-eng/grpc-web v0.0.0-20181111100011-16092bd1d58a - github.com/itchyny/gojq v0.12.10 + github.com/itchyny/gojq v0.12.12 github.com/jeremywohl/flatten v1.0.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/ktrysmt/go-bitbucket v0.9.55 - github.com/mattn/go-isatty v0.0.16 + github.com/mattn/go-isatty v0.0.19 github.com/mattn/go-zglob v0.0.4 github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 github.com/olekukonko/tablewriter v0.0.5 @@ -67,9 +66,9 @@ require ( github.com/soheilhy/cmux v0.1.5 github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.4 github.com/valyala/fasttemplate v1.2.2 - github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 + github.com/whilp/git-urls v1.0.0 github.com/xanzy/go-gitlab v0.83.0 github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.31.0 @@ -138,6 +137,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect github.com/fvbommel/sortorder v1.0.1 // indirect + github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-git/gcfg v1.5.0 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect @@ -201,7 +201,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rs/cors v1.8.0 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -289,4 +289,4 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 -) +) \ No newline at end of file From 84bb8a3693c6b13dc6bb2a41bc3ee7a41aea04f5 Mon Sep 17 00:00:00 2001 From: reggie Date: Fri, 2 Jun 2023 16:57:29 +0300 Subject: [PATCH 35/38] gitops engine dependency and test fixes Signed-off-by: reggie --- go.mod | 4 ++-- go.sum | 4 ++-- server/application/application_test.go | 2 +- util/lua/custom_actions_test.go | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index cc207f6e8b36b..f201983351705 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.23.1 github.com/antonmedv/expr v1.9.0 - github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc + github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d github.com/aws/aws-sdk-go v1.44.271 @@ -289,4 +289,4 @@ replace ( k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 -) \ No newline at end of file +) diff --git a/go.sum b/go.sum index 453e3f99e72b0..ec101944ca40b 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc h1:i6OgOvFFsoWiGBFEhhDNcYoRtxxtrVwcD7wCEeqhct4= -github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00 h1:txH/SJQJJTdqmKnbQ8sKYGAgjBy4VvWtS45trc5yW/0= +github.com/argoproj/gitops-engine v0.7.1-0.20230527174809-e56739ceba00/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da h1:Vf9xvHcXn4TP/nLIfWn+TaC521V9fpz/DwRP6uEeVR8= github.com/argoproj/notifications-engine v0.4.1-0.20230228182525-f754726f03da/go.mod h1:05koR0gE/O0i5YDbidg1dpr76XitK4DJveh+dIAq6e8= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds= diff --git a/server/application/application_test.go b/server/application/application_test.go index 85570bc74967f..c620bb8e97ce7 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -2101,7 +2101,7 @@ func TestRunNewStyleResourceAction(t *testing.T) { Kind: &kind, }) - assert.Equal(t, permissionDeniedErr.Error(), runErr.Error()) + assert.Contains(t, runErr.Error(), "is not permitted to manage") assert.Nil(t, appResponse) }) diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 1f04f3b11b939..8886187807526 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -8,8 +8,6 @@ import ( "strings" "testing" - "github.com/ghodss/yaml" - "github.com/argoproj/gitops-engine/pkg/diff" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" From 76c0aa219b061ee972e8367504f5c5a9c639949b Mon Sep 17 00:00:00 2001 From: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Date: Mon, 5 Jun 2023 14:40:32 -0400 Subject: [PATCH 36/38] add workflows action Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --- .../CronWorkflow/actions/action_test.yaml | 4 ++ .../actions/create-workflow/action.lua | 56 +++++++++++++++++++ .../CronWorkflow/actions/discovery.lua | 3 + .../actions/testdata/cronjob.yaml | 22 ++++++++ .../CronWorkflow/actions/testdata/job.yaml | 19 +++++++ 5 files changed, 104 insertions(+) create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/discovery.lua create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml new file mode 100644 index 0000000000000..a9b5320db5721 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml @@ -0,0 +1,4 @@ +actionTests: +- action: create-job + inputPath: testdata/cronjob.yaml + expectedOutputPath: testdata/job.yaml diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua b/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua new file mode 100644 index 0000000000000..3ee6af36cbaa4 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua @@ -0,0 +1,56 @@ +local os = require("os") + +-- This action constructs a Workflow resource from a CronWorkflow resource, to enable creating a CronWorkflow instance +-- on demand. +-- It returns an array with a single member - a table with the operation to perform (create) and the Workflow resource. +-- It mimics the output of "argo submit --from=CronWorkflow/" command, declaratively. + +-- Deep-copying an object is a ChatGPT generated code. +-- Since empty tables are treated as empty arrays, the resulting k8s resource might be invalid (arrays instead of maps). +-- So empty tables are not cloned to the target object. +function deepCopy(object) + local lookup_table = {} + local function _copy(obj) + if type(obj) ~= "table" then + return obj + elseif lookup_table[obj] then + return lookup_table[obj] + elseif next(obj) == nil then + return nil + else + local new_table = {} + lookup_table[obj] = new_table + for key, value in pairs(obj) do + new_table[_copy(key)] = _copy(value) + end + return setmetatable(new_table, getmetatable(obj)) + end + end + return _copy(object) +end + +workflow = {} +workflow.apiVersion = "argoproj.io/v1alpha1" +workflow.kind = "Workflow" + +workflow.metadata = {} +workflow.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") +workflow.metadata.namespace = obj.metadata.namespace + +ownerRef = {} +ownerRef.apiVersion = obj.apiVersion +ownerRef.kind = obj.kind +ownerRef.name = obj.metadata.name +ownerRef.uid = obj.metadata.uid +workflow.metadata.ownerReferences = {} +workflow.metadata.ownerReferences[1] = ownerRef + +workflow.spec = deepCopy(obj.spec.workflowSpec) + +impactedResource = {} +impactedResource.operation = "create" +impactedResource.resource = workflow +result = {} +result[1] = impactedResource + +return result \ No newline at end of file diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/discovery.lua b/resource_customizations/argoproj.io/CronWorkflow/actions/discovery.lua new file mode 100644 index 0000000000000..5e16c6c1c14d8 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/discovery.lua @@ -0,0 +1,3 @@ +actions = {} +actions["create-workflow"] = {} +return actions \ No newline at end of file diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml new file mode 100644 index 0000000000000..118fc83929e96 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml @@ -0,0 +1,22 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello + namespace: test-ns + uid: "123" +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + resources: {} + restartPolicy: OnFailure \ No newline at end of file diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml new file mode 100644 index 0000000000000..cf0f92da24818 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml @@ -0,0 +1,19 @@ +- k8sOperation: create + unstructuredObj: + apiVersion: batch/v1 + kind: Job + metadata: + name: hello-00000000000 + namespace: test-ns + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 + imagePullPolicy: IfNotPresent + command: + - /bin/sh + - -c + - date; echo Hello from the Kubernetes cluster + restartPolicy: OnFailure \ No newline at end of file From aa070507c8398ac88f42d1570b93c01c17da2928 Mon Sep 17 00:00:00 2001 From: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Date: Thu, 22 Jun 2023 14:37:00 -0400 Subject: [PATCH 37/38] cronworkflow and workflowtemplate actions Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --- docs/operator-manual/resource_actions.md | 51 +++++++++++++------ docs/operator-manual/upgrading/2.7-2.8.md | 44 +++++++++++++++- .../CronWorkflow/actions/action_test.yaml | 6 +-- .../actions/create-workflow/action.lua | 26 ++++++++++ .../actions/testdata/cronjob.yaml | 22 -------- .../actions/testdata/cronworkflow.yaml | 34 +++++++++++++ .../CronWorkflow/actions/testdata/job.yaml | 19 ------- .../actions/testdata/workflow.yaml | 28 ++++++++++ .../WorkflowTemplate/actions/action_test.yaml | 4 ++ .../actions/create-workflow/action.lua | 39 ++++++++++++++ .../WorkflowTemplate/actions/discovery.lua | 3 ++ .../actions/testdata/workflow.yaml | 16 ++++++ .../actions/testdata/workflowtemplate.yaml | 24 +++++++++ util/lua/custom_actions_test.go | 31 ++++++++--- 14 files changed, 280 insertions(+), 67 deletions(-) delete mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronworkflow.yaml delete mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml create mode 100644 resource_customizations/argoproj.io/CronWorkflow/actions/testdata/workflow.yaml create mode 100644 resource_customizations/argoproj.io/WorkflowTemplate/actions/action_test.yaml create mode 100644 resource_customizations/argoproj.io/WorkflowTemplate/actions/create-workflow/action.lua create mode 100644 resource_customizations/argoproj.io/WorkflowTemplate/actions/discovery.lua create mode 100644 resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflow.yaml create mode 100644 resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflowtemplate.yaml diff --git a/docs/operator-manual/resource_actions.md b/docs/operator-manual/resource_actions.md index 8f3862450e434..b720f589ae8d0 100644 --- a/docs/operator-manual/resource_actions.md +++ b/docs/operator-manual/resource_actions.md @@ -19,11 +19,14 @@ You can define your own custom resource actions in the `argocd-cm` ConfigMap. ### Custom Resource Action Types #### An action that modifies the source resource + This action modifies and returns the source resource. This kind of action was the only one available till 2.8, and it is still supported. #### An action that produces a list of new or modified resources -An alpha feature, introduced in 2.8. + +**An alpha feature, introduced in 2.8.** + This action returns a list of impacted resources, each impacted resource has a K8S resource and an operation to perform on. Currently supported operations are "create" and "patch", "patch" is only supported for the source resource. Creating new resources is possible, by specifying a "create" operation for each such resource in the returned list. @@ -70,14 +73,22 @@ The `discovery.lua` script must return a table where the key name represents the Each action name must be represented in the list of `definitions` with an accompanying `action.lua` script to control the resource modifications. The `obj` is a global variable which contains the resource. Each action script returns an optionally modified version of the resource. In this example, we are simply setting `.spec.suspend` to either `true` or `false`. #### Creating new resources with a custom action + +!!! important + Creating resources via the Argo CD UI is an intentional, strategic departure from GitOps principles. We recommend + that you use this feature sparingly and only for resources that are not part of the desired state of the + application. + The resource the action is invoked on would be referred to as the `source resource`. The new resource and all the resources implicitly created as a result, must be permitted on the AppProject level, otherwise the creation will fail. ##### Creating a source resource child resources with a custom action + If the new resource represents a k8s child of the source resource, the source resource ownerReference must be set on the new resource. -Here is an example Lua snippet, that takes care of constructing a Job resource that is a child of a source CronJob resource - the `obj` is a global variable, which contains the source resource: -``` -... +Here is an example Lua snippet, that takes care of constructing a Job resource that is a child of a source CronJob resource - the `obj` is a global variable, which contains the source resource: + +```lua +-- ... ownerRef = {} ownerRef.apiVersion = obj.apiVersion ownerRef.kind = obj.kind @@ -87,40 +98,48 @@ job = {} job.metadata = {} job.metadata.ownerReferences = {} job.metadata.ownerReferences[1] = ownerRef -... +-- ... ``` + ##### Creating independent child resources with a custom action + If the new resource is independent of the source resource, the default behavior of such new resource is that it is not known by the App of the source resource (as it is not part of the desired state and does not have an `ownerReference`). To make the App aware of the new resource, the `app.kubernetes.io/instance` label (or other ArgoCD tracking label, if configured) must be set on the resource. It can be copied from the source resource, like this: -``` -... + +```lua +-- ... newObj = {} newObj.metadata = {} newObj.metadata.labels = {} newObj.metadata.labels["app.kubernetes.io/instance"] = obj.metadata.labels["app.kubernetes.io/instance"] -... +-- ... ``` + While the new resource will be part of the App with the tracking label in place, it will be immediately deleted if auto prune is set on the App. To keep the resource, set `Prune=false` annotation on the resource, with this Lua snippet: -``` -... + +```lua +-- ... newObj.metadata.annotations = {} newObj.metadata.annotations["argocd.argoproj.io/sync-options"] = "Prune=false" -... +-- ... ``` + (If setting `Prune=false` behavior, the resource will not be deleted upon the deletion of the App, and will require a manual cleanup). The resource and the App will now appear out of sync - which is the expected ArgoCD behavior upon creating a resource that is not part of the desired state. -If you wish to treat such an App as a synced one, add the following resource annotation in Lua code: -``` -... -newObj.metadata.annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" -... +If you wish to treat such an App as a synced one, add the following resource annotation in Lua code: + +```lua +-- ... +newObj.metadata.annotations["argocd.argoproj.io/compare-options"] = "IgnoreExtraneous" +-- ... ``` #### An action that produces a list of resources - a complete example: + ```yaml resource.customizations.actions.ConfigMap: | discovery.lua: | diff --git a/docs/operator-manual/upgrading/2.7-2.8.md b/docs/operator-manual/upgrading/2.7-2.8.md index 58c5098982246..41e22218c3d77 100644 --- a/docs/operator-manual/upgrading/2.7-2.8.md +++ b/docs/operator-manual/upgrading/2.7-2.8.md @@ -15,4 +15,46 @@ properly before moving to 2.8. Prior to `v2.8`, the `List` endpoint on the `ClusterService` did **not** filter clusters when responding, despite accepting query parameters. This bug has been addressed, and query parameters are now taken into account to filter the -resulting list of clusters. \ No newline at end of file +resulting list of clusters. + +## Configure RBAC to account for new actions + +2.8 introduces three new actions: +* Create a Job from a CronJob +* Create a Workflow from a CronWorkflow +* Create a Workflow from a WorkflowTemplate + +When you upgrade to 2.8, RBAC policies with `applications` in the *resource* +field and `*` or `action/*` in the action field, it will automatically grant the +ability to use these new actions. + +If you would like to avoid granting these new permissions, you can update your RBAC policies to be more specific. + +### Example + +Old: + +```csv +p, role:action-runner, applications, actions/, *, allow +``` + +New: + +```csv +p, role:action-runner, applications, action/argoproj.io/Rollout/abort, *, allow +p, role:action-runner, applications, action/argoproj.io/Rollout/promote-full, *, allow +p, role:action-runner, applications, action/argoproj.io/Rollout/retry, *, allow +p, role:action-runner, applications, action/argoproj.io/Rollout/resume, *, allow +p, role:action-runner, applications, action/argoproj.io/Rollout/restart, *, allow +p, role:action-runner, applications, action/argoproj.io/AnalysisRun/terminate, *, allow +p, role:action-runner, applications, action/apps/DaemonSet/restart, *, allow +p, role:action-runner, applications, action/apps/StatefulSet/restart, *, allow +p, role:action-runner, applications, action/apps/Deployment/pause, *, allow +p, role:action-runner, applications, action/apps/Deployment/resume, *, allow +p, role:action-runner, applications, action/apps/Deployment/restart, *, allow + +# If you don't want to grant the new permissions, don't include the following lines +p, role:action-runner, applications, action/argoproj.io/WorkflowTemplate/create-workflow, *, allow +p, role:action-runner, applications, action/argoproj.io/CronWorkflow/create-workflow, *, allow +p, role:action-runner, applications, action/batch/CronJob/create-job, *, allow +``` diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml index a9b5320db5721..4c7aa77ff127a 100644 --- a/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/action_test.yaml @@ -1,4 +1,4 @@ actionTests: -- action: create-job - inputPath: testdata/cronjob.yaml - expectedOutputPath: testdata/job.yaml +- action: create-workflow + inputPath: testdata/cronworkflow.yaml + expectedOutputPath: testdata/workflow.yaml diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua b/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua index 3ee6af36cbaa4..35f7a66b80413 100644 --- a/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/create-workflow/action.lua @@ -5,6 +5,9 @@ local os = require("os") -- It returns an array with a single member - a table with the operation to perform (create) and the Workflow resource. -- It mimics the output of "argo submit --from=CronWorkflow/" command, declaratively. +-- This code is written to mimic what the Argo Workflows API server does to create a Workflow from a CronWorkflow. +-- https://github.com/argoproj/argo-workflows/blob/873a58de7dd9dad76d5577b8c4294a58b52849b8/workflow/common/convert.go#L12 + -- Deep-copying an object is a ChatGPT generated code. -- Since empty tables are treated as empty arrays, the resulting k8s resource might be invalid (arrays instead of maps). -- So empty tables are not cloned to the target object. @@ -36,6 +39,29 @@ workflow.kind = "Workflow" workflow.metadata = {} workflow.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") workflow.metadata.namespace = obj.metadata.namespace +workflow.metadata.labels = {} +workflow.metadata.annotations = {} +if (obj.spec.workflowMetadata ~= nil) then + if (obj.spec.workflowMetadata.labels ~= nil) then + workflow.metadata.labels = deepCopy(obj.spec.workflowMetadata.labels) + end + if (obj.spec.workflowMetadata.annotations ~= nil) then + workflow.metadata.annotations = deepCopy(obj.spec.workflowMetadata.annotations) + end +end +workflow.metadata.labels["workflows.argoproj.io/cron-workflow"] = obj.metadata.name +if (obj.metadata.labels["workflows.argoproj.io/controller-instanceid"] ~= nil) then + workflow.metadata.labels["workflows.argoproj.io/controller-instanceid"] = obj.metadata.labels["workflows.argoproj.io/controller-instanceid"] +end +workflow.metadata.annotations["workflows.argoproj.io/scheduled-time"] = os.date("!%Y-%m-%dT%d:%H:%MZ") + +workflow.finalizers = {} +-- add all finalizers from obj.spec.workflowMetadata.finalizers +if (obj.spec.workflowMetadata ~= nil and obj.spec.workflowMetadata.finalizers ~= nil) then + for i, finalizer in ipairs(obj.spec.workflowMetadata.finalizers) do + workflow.finalizers[i] = finalizer + end +end ownerRef = {} ownerRef.apiVersion = obj.apiVersion diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml deleted file mode 100644 index 118fc83929e96..0000000000000 --- a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronjob.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: batch/v1 -kind: CronJob -metadata: - name: hello - namespace: test-ns - uid: "123" -spec: - schedule: "* * * * *" - jobTemplate: - spec: - template: - spec: - containers: - - name: hello - image: busybox:1.28 - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -c - - date; echo Hello from the Kubernetes cluster - resources: {} - restartPolicy: OnFailure \ No newline at end of file diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronworkflow.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronworkflow.yaml new file mode 100644 index 0000000000000..2a2c7d1807db4 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/cronworkflow.yaml @@ -0,0 +1,34 @@ +apiVersion: argoproj.io/v1alpha1 +kind: CronWorkflow +metadata: + annotations: + cronworkflows.argoproj.io/last-used-schedule: CRON_TZ=America/Los_Angeles * * * * * + labels: + workflows.argoproj.io/controller-instanceid: test-instance + app.kubernetes.io/instance: test + name: hello-world + namespace: default +spec: + concurrencyPolicy: Replace + failedJobsHistoryLimit: 4 + schedule: '* * * * *' + startingDeadlineSeconds: 0 + successfulJobsHistoryLimit: 4 + suspend: true + timezone: America/Los_Angeles + workflowSpec: + entrypoint: whalesay + templates: + - container: + args: + - "\U0001F553 hello world. Scheduled on: {{workflow.scheduledTime}}" + command: + - cowsay + image: 'docker/whalesay:latest' + name: whalesay + workflowMetadata: + labels: + example: test + annotations: + another-example: another-test + finalizers: [test-finalizer] diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml deleted file mode 100644 index cf0f92da24818..0000000000000 --- a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/job.yaml +++ /dev/null @@ -1,19 +0,0 @@ -- k8sOperation: create - unstructuredObj: - apiVersion: batch/v1 - kind: Job - metadata: - name: hello-00000000000 - namespace: test-ns - spec: - template: - spec: - containers: - - name: hello - image: busybox:1.28 - imagePullPolicy: IfNotPresent - command: - - /bin/sh - - -c - - date; echo Hello from the Kubernetes cluster - restartPolicy: OnFailure \ No newline at end of file diff --git a/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/workflow.yaml b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/workflow.yaml new file mode 100644 index 0000000000000..9f231dbb5c5b3 --- /dev/null +++ b/resource_customizations/argoproj.io/CronWorkflow/actions/testdata/workflow.yaml @@ -0,0 +1,28 @@ +- k8sOperation: create + unstructuredObj: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + annotations: + another-example: another-test + labels: + workflows.argoproj.io/cron-workflow: hello-world + workflows.argoproj.io/controller-instanceid: test-instance + example: test + name: hello-world-202306221736 + namespace: default + ownerReferences: + - apiVersion: argoproj.io/v1alpha1 + kind: CronWorkflow + name: hello-world + finalizers: [test-finalizer] + spec: + entrypoint: whalesay + templates: + - container: + args: + - "\U0001F553 hello world. Scheduled on: {{workflow.scheduledTime}}" + command: + - cowsay + image: 'docker/whalesay:latest' + name: whalesay diff --git a/resource_customizations/argoproj.io/WorkflowTemplate/actions/action_test.yaml b/resource_customizations/argoproj.io/WorkflowTemplate/actions/action_test.yaml new file mode 100644 index 0000000000000..db503fe0b6aae --- /dev/null +++ b/resource_customizations/argoproj.io/WorkflowTemplate/actions/action_test.yaml @@ -0,0 +1,4 @@ +actionTests: +- action: create-workflow + inputPath: testdata/workflowtemplate.yaml + expectedOutputPath: testdata/workflow.yaml diff --git a/resource_customizations/argoproj.io/WorkflowTemplate/actions/create-workflow/action.lua b/resource_customizations/argoproj.io/WorkflowTemplate/actions/create-workflow/action.lua new file mode 100644 index 0000000000000..66f5ec55d3a16 --- /dev/null +++ b/resource_customizations/argoproj.io/WorkflowTemplate/actions/create-workflow/action.lua @@ -0,0 +1,39 @@ +local os = require("os") + +-- This action constructs a Workflow resource from a WorkflowTemplate resource, to enable creating a WorkflowTemplate instance +-- on demand. +-- It returns an array with a single member - a table with the operation to perform (create) and the Workflow resource. +-- It mimics the output of "argo submit --from=workflowtemplate/" command, declaratively. + +-- This code is written to mimic what the Argo Workflows API server does to create a Workflow from a WorkflowTemplate. +-- https://github.com/argoproj/argo-workflows/blob/873a58de7dd9dad76d5577b8c4294a58b52849b8/workflow/common/convert.go#L34 + +workflow = {} +workflow.apiVersion = "argoproj.io/v1alpha1" +workflow.kind = "Workflow" + +workflow.metadata = {} +workflow.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") +workflow.metadata.namespace = obj.metadata.namespace +workflow.metadata.labels = {} +workflow.metadata.labels["workflows.argoproj.io/workflow-template"] = obj.metadata.name + +workflow.spec = {} +workflow.spec.workflowTemplateRef = {} +workflow.spec.workflowTemplateRef.name = obj.metadata.name + +ownerRef = {} +ownerRef.apiVersion = obj.apiVersion +ownerRef.kind = obj.kind +ownerRef.name = obj.metadata.name +ownerRef.uid = obj.metadata.uid +workflow.metadata.ownerReferences = {} +workflow.metadata.ownerReferences[1] = ownerRef + +impactedResource = {} +impactedResource.operation = "create" +impactedResource.resource = workflow +result = {} +result[1] = impactedResource + +return result \ No newline at end of file diff --git a/resource_customizations/argoproj.io/WorkflowTemplate/actions/discovery.lua b/resource_customizations/argoproj.io/WorkflowTemplate/actions/discovery.lua new file mode 100644 index 0000000000000..5e16c6c1c14d8 --- /dev/null +++ b/resource_customizations/argoproj.io/WorkflowTemplate/actions/discovery.lua @@ -0,0 +1,3 @@ +actions = {} +actions["create-workflow"] = {} +return actions \ No newline at end of file diff --git a/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflow.yaml b/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflow.yaml new file mode 100644 index 0000000000000..46063bee03397 --- /dev/null +++ b/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflow.yaml @@ -0,0 +1,16 @@ +- k8sOperation: create + unstructuredObj: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + labels: + workflows.argoproj.io/workflow-template: workflow-template-submittable + name: workflow-template-submittable-202306221735 + namespace: default + ownerReferences: + - apiVersion: argoproj.io/v1alpha1 + kind: WorkflowTemplate + name: workflow-template-submittable + spec: + workflowTemplateRef: + name: workflow-template-submittable diff --git a/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflowtemplate.yaml b/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflowtemplate.yaml new file mode 100644 index 0000000000000..5b7d2319e9c9e --- /dev/null +++ b/resource_customizations/argoproj.io/WorkflowTemplate/actions/testdata/workflowtemplate.yaml @@ -0,0 +1,24 @@ +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + labels: + app.kubernetes.io/instance: test + name: workflow-template-submittable + namespace: default +spec: + arguments: + parameters: + - name: message + value: hello world + entrypoint: whalesay-template + templates: + - container: + args: + - '{{inputs.parameters.message}}' + command: + - cowsay + image: docker/whalesay + inputs: + parameters: + - name: message + name: whalesay-template diff --git a/util/lua/custom_actions_test.go b/util/lua/custom_actions_test.go index 8886187807526..7dd2c41f623e2 100644 --- a/util/lua/custom_actions_test.go +++ b/util/lua/custom_actions_test.go @@ -12,10 +12,11 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/yaml" + "github.com/argoproj/gitops-engine/pkg/diff" + appsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/util/cli" "github.com/argoproj/argo-cd/v2/util/errors" - "github.com/argoproj/gitops-engine/pkg/diff" ) type testNormalizer struct{} @@ -57,6 +58,19 @@ func (t testNormalizer) Normalize(un *unstructured.Unstructured) error { if err != nil { return fmt.Errorf("failed to normalize Rollout: %w", err) } + case "Workflow": + err := unstructured.SetNestedField(un.Object, nil, "metadata", "resourceVersion") + if err != nil { + return fmt.Errorf("failed to normalize Rollout: %w", err) + } + err = unstructured.SetNestedField(un.Object, nil, "metadata", "uid") + if err != nil { + return fmt.Errorf("failed to normalize Rollout: %w", err) + } + err = unstructured.SetNestedField(un.Object, nil, "metadata", "annotations", "workflows.argoproj.io/scheduled-time") + if err != nil { + return fmt.Errorf("failed to normalize Rollout: %w", err) + } } return nil } @@ -137,10 +151,10 @@ func TestLuaResourceActionsScript(t *testing.T) { // The expected output is a list of objects // Find the actual impacted resource in the expected output expectedObj := findFirstMatchingItem(expectedObjects.Items, func(u unstructured.Unstructured) bool { - // Job's name is derived from the CronJob name, so the returned Job name is not actually equal to the testdata output name - // Considering the Job found in the testdata output if its name starts with CronJob name - // TODO: maybe this should use a normalizer function instead of hard-coding the Job specifics here - if result.GetKind() == "Job" && sourceObj.GetKind() == "CronJob" { + // Some resources' name is derived from the source object name, so the returned name is not actually equal to the testdata output name + // Considering the resource found in the testdata output if its name starts with source object name + // TODO: maybe this should use a normalizer function instead of hard-coding the resource specifics here + if (result.GetKind() == "Job" && sourceObj.GetKind() == "CronJob") || (result.GetKind() == "Workflow" && (sourceObj.GetKind() == "CronWorkflow" || sourceObj.GetKind() == "WorkflowTemplate")) { return u.GroupVersionKind() == result.GroupVersionKind() && strings.HasPrefix(u.GetName(), sourceObj.GetName()) && u.GetNamespace() == result.GetNamespace() } else { return u.GroupVersionKind() == result.GroupVersionKind() && u.GetName() == result.GetName() && u.GetNamespace() == result.GetNamespace() @@ -157,7 +171,12 @@ func TestLuaResourceActionsScript(t *testing.T) { assert.EqualValues(t, sourceObj.GetName(), result.GetName()) assert.EqualValues(t, sourceObj.GetNamespace(), result.GetNamespace()) case CreateOperation: - // no special logic to test for now + switch result.GetKind() { + case "Job": + case "Workflow": + // The name of the created resource is derived from the source object name, so the returned name is not actually equal to the testdata output name + result.SetName(expectedObj.GetName()) + } } // Ideally, we would use a assert.Equal to detect the difference, but the Lua VM returns a object with float64 instead of the original int32. As a result, the assert.Equal is never true despite that the change has been applied. diffResult, err := diff.Diff(expectedObj, result, diff.WithNormalizer(testNormalizer{})) From cd2302f295454d892788b241bff7e8966d55f501 Mon Sep 17 00:00:00 2001 From: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Date: Fri, 23 Jun 2023 10:06:35 -0400 Subject: [PATCH 38/38] update gitops-engine Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0833fdfaf494c..76dd36e7036bb 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.30.3 github.com/antonmedv/expr v1.12.5 - github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc + github.com/argoproj/gitops-engine v0.7.1-0.20230607163028-425d65e07695 github.com/argoproj/notifications-engine v0.4.1-0.20230620204159-3446d4ae8520 github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d github.com/aws/aws-sdk-go v1.44.288 diff --git a/go.sum b/go.sum index 113ddb77d72f9..e8d71cbba49d0 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m github.com/antonmedv/expr v1.12.5 h1:Fq4okale9swwL3OeLLs9WD9H6GbgBLJyN/NUHRv+n0E= github.com/antonmedv/expr v1.12.5/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc h1:i6OgOvFFsoWiGBFEhhDNcYoRtxxtrVwcD7wCEeqhct4= -github.com/argoproj/gitops-engine v0.7.1-0.20230526233214-ad9a694fe4bc/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= +github.com/argoproj/gitops-engine v0.7.1-0.20230607163028-425d65e07695 h1:w8OPbqHyhWxLyC4LZgs5JBUe7AOkJpNZqFa92yy7Kmc= +github.com/argoproj/gitops-engine v0.7.1-0.20230607163028-425d65e07695/go.mod h1:WpA/B7tgwfz+sdNE3LqrTrb7ArEY1FOPI2pAGI0hfPc= github.com/argoproj/notifications-engine v0.4.1-0.20230620204159-3446d4ae8520 h1:ZCpg1Zk78E8QxMI52w6ZIddxkBHv27YWmfWQdxxWUkw= github.com/argoproj/notifications-engine v0.4.1-0.20230620204159-3446d4ae8520/go.mod h1:sbhf4EjAUGAqRdHIzifDIiWsjlsTfmytVJJCCiUdyVA= github.com/argoproj/pkg v0.13.7-0.20221221191914-44694015343d h1:7fXEKF3OQ9i1PrgieA6FLrXOL3UAKyiotomn0RHevds=