mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
feat(appset): Implement Plugin Generator (#13017)
* add internal http package Signed-off-by: Maxence Laude <maxence@laude.pro> * add services plugin Signed-off-by: Maxence Laude <maxence@laude.pro> * add generator plugin Signed-off-by: Maxence Laude <maxence@laude.pro> * adapted matrix && merge generator Signed-off-by: Maxence Laude <maxence@laude.pro> * adapted plugin to webhook Signed-off-by: Maxence Laude <maxence@laude.pro> * update applicationset controller and types for plugin Signed-off-by: Maxence Laude <maxence@laude.pro> * add proposal for applicationset plugin generator Signed-off-by: Maxence Laude <maxence@laude.pro> * execute codegen Signed-off-by: Maxence Laude <maxence@laude.pro> * First draft of documentation Signed-off-by: Maxence Laude <maxence@laude.pro> * Fix wrong expected error on client_test Signed-off-by: Maxence Laude <maxence@laude.pro> * docs(plugin-generator): minor improvements Signed-off-by: Sébastien Crocquesel <88554524+scrocquesel@users.noreply.github.com> * Improvement * changes Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * fix docs Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * wrap output Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * fix test Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * fix tests Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * nested parameters Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * simplify Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * docs Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --------- Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * Add plugin to GetRequeueAfter function (merge && matrix) Signed-off-by: Maxence Laude <maxence@laude.pro> * Improvement : renaming * more changes Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * clearer docs Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * abstract Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * naming Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * revert accidental change Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * ugh Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * fix accidental renames Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --------- Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * Fix typo renaming Signed-off-by: Maxence Laude <maxence@laude.pro> * Improve docs Signed-off-by: Maxence Laude <maxence@laude.pro> * Webhook implementation Signed-off-by: Maxence Laude <maxence@laude.pro> * Typo docs Signed-off-by: Maxence Laude <maxence@laude.pro> * fix plugin generator nil panic Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * Add company to USERS.md Signed-off-by: Maxence Laude <maxence@laude.pro> * input.parameters * fix plugin generator nil panic Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * input.parameters Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --------- Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * Change param structure * change param structure Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * nest parameters Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> --------- Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> * Fix conflicts Signed-off-by: Maxence Laude <maxence@laude.pro> * Fix docs Signed-off-by: Maxence Laude <maxence@laude.pro> * Fix docs Signed-off-by: Maxence Laude <maxence@laude.pro> --------- Signed-off-by: Maxence Laude <maxence@laude.pro> Signed-off-by: Sébastien Crocquesel <88554524+scrocquesel@users.noreply.github.com> Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Co-authored-by: Sébastien Crocquesel <88554524+scrocquesel@users.noreply.github.com> Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
parent
8e8970e2f4
commit
ec2340a11f
34 changed files with 10368 additions and 732 deletions
1
USERS.md
1
USERS.md
|
|
@ -175,6 +175,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
|||
1. [Objective](https://www.objective.com.br/)
|
||||
1. [OCCMundial](https://occ.com.mx)
|
||||
1. [Octadesk](https://octadesk.com)
|
||||
1. [Olfeo](https://www.olfeo.com/)
|
||||
1. [omegaUp](https://omegaUp.com)
|
||||
1. [Omni](https://omni.se/)
|
||||
1. [openEuler](https://openeuler.org)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ func (m *MatrixGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.Appli
|
|||
SCMProvider: appSetBaseGenerator.SCMProvider,
|
||||
ClusterDecisionResource: appSetBaseGenerator.ClusterDecisionResource,
|
||||
PullRequest: appSetBaseGenerator.PullRequest,
|
||||
Plugin: appSetBaseGenerator.Plugin,
|
||||
Matrix: matrixGen,
|
||||
Merge: mergeGen,
|
||||
Selector: appSetBaseGenerator.Selector,
|
||||
|
|
@ -135,6 +136,7 @@ func (m *MatrixGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Ap
|
|||
Clusters: r.Clusters,
|
||||
Git: r.Git,
|
||||
PullRequest: r.PullRequest,
|
||||
Plugin: r.Plugin,
|
||||
Matrix: matrixGen,
|
||||
Merge: mergeGen,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/services/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -14,6 +13,8 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/services/mocks"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
|
|
@ -848,7 +849,7 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
|
|||
}
|
||||
|
||||
listGenerator := &argoprojiov1alpha1.ListGenerator{
|
||||
Elements: []apiextensionsv1.JSON{},
|
||||
Elements: []apiextensionsv1.JSON{},
|
||||
ElementsYaml: "{{ .foo.bar | toJson }}",
|
||||
}
|
||||
|
||||
|
|
@ -870,60 +871,59 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
|
|||
},
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"chart": "a",
|
||||
"version": "1",
|
||||
"chart": "a",
|
||||
"version": "1",
|
||||
"foo": map[string]interface{}{
|
||||
"bar": []interface{}{
|
||||
map[string]interface{}{
|
||||
"chart": "a",
|
||||
"chart": "a",
|
||||
"version": "1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"chart": "b",
|
||||
"chart": "b",
|
||||
"version": "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"path": map[string]interface{}{
|
||||
"basename": "dir",
|
||||
"basename": "dir",
|
||||
"basenameNormalized": "dir",
|
||||
"filename": "file_name.yaml",
|
||||
"filename": "file_name.yaml",
|
||||
"filenameNormalized": "file-name.yaml",
|
||||
"path": "path/dir",
|
||||
"segments": []string {
|
||||
"path": "path/dir",
|
||||
"segments": []string{
|
||||
"path",
|
||||
"dir",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"chart": "b",
|
||||
"version": "2",
|
||||
"chart": "b",
|
||||
"version": "2",
|
||||
"foo": map[string]interface{}{
|
||||
"bar": []interface{}{
|
||||
map[string]interface{}{
|
||||
"chart": "a",
|
||||
"chart": "a",
|
||||
"version": "1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"chart": "b",
|
||||
"chart": "b",
|
||||
"version": "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"path": map[string]interface{}{
|
||||
"basename": "dir",
|
||||
"basename": "dir",
|
||||
"basenameNormalized": "dir",
|
||||
"filename": "file_name.yaml",
|
||||
"filename": "file_name.yaml",
|
||||
"filenameNormalized": "file-name.yaml",
|
||||
"path": "path/dir",
|
||||
"segments": []string {
|
||||
"path": "path/dir",
|
||||
"segments": []string{
|
||||
"path",
|
||||
"dir",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -952,27 +952,26 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
|
|||
"foo": map[string]interface{}{
|
||||
"bar": []interface{}{
|
||||
map[string]interface{}{
|
||||
"chart": "a",
|
||||
"chart": "a",
|
||||
"version": "1",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"chart": "b",
|
||||
"chart": "b",
|
||||
"version": "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"path": map[string]interface{}{
|
||||
"basename": "dir",
|
||||
"basename": "dir",
|
||||
"basenameNormalized": "dir",
|
||||
"filename": "file_name.yaml",
|
||||
"filename": "file_name.yaml",
|
||||
"filenameNormalized": "file-name.yaml",
|
||||
"path": "path/dir",
|
||||
"segments": []string {
|
||||
"path": "path/dir",
|
||||
"segments": []string{
|
||||
"path",
|
||||
"dir",
|
||||
},
|
||||
},
|
||||
|
||||
}}, nil)
|
||||
genMock.On("GetTemplate", &gitGeneratorSpec).
|
||||
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ func (m *MergeGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.Applic
|
|||
SCMProvider: appSetBaseGenerator.SCMProvider,
|
||||
ClusterDecisionResource: appSetBaseGenerator.ClusterDecisionResource,
|
||||
PullRequest: appSetBaseGenerator.PullRequest,
|
||||
Plugin: appSetBaseGenerator.Plugin,
|
||||
Matrix: matrixGen,
|
||||
Merge: mergeGen,
|
||||
Selector: appSetBaseGenerator.Selector,
|
||||
|
|
@ -190,6 +191,7 @@ func (m *MergeGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.App
|
|||
Clusters: r.Clusters,
|
||||
Git: r.Git,
|
||||
PullRequest: r.PullRequest,
|
||||
Plugin: r.Plugin,
|
||||
Matrix: matrixGen,
|
||||
Merge: mergeGen,
|
||||
}
|
||||
|
|
|
|||
211
applicationset/generators/plugin.go
Normal file
211
applicationset/generators/plugin.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package generators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jeremywohl/flatten"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/util/settings"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/services/plugin"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultPluginRequeueAfterSeconds = 30 * time.Minute
|
||||
)
|
||||
|
||||
var _ Generator = (*PluginGenerator)(nil)
|
||||
|
||||
type PluginGenerator struct {
|
||||
client client.Client
|
||||
ctx context.Context
|
||||
clientset kubernetes.Interface
|
||||
namespace string
|
||||
}
|
||||
|
||||
func NewPluginGenerator(client client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
|
||||
g := &PluginGenerator{
|
||||
client: client,
|
||||
ctx: ctx,
|
||||
clientset: clientset,
|
||||
namespace: namespace,
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
|
||||
// Return a requeue default of 30 minutes, if no default is specified.
|
||||
|
||||
if appSetGenerator.Plugin.RequeueAfterSeconds != nil {
|
||||
return time.Duration(*appSetGenerator.Plugin.RequeueAfterSeconds) * time.Second
|
||||
}
|
||||
|
||||
return DefaultPluginRequeueAfterSeconds
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
|
||||
return &appSetGenerator.Plugin.Template
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) ([]map[string]interface{}, error) {
|
||||
|
||||
if appSetGenerator == nil {
|
||||
return nil, EmptyAppSetGeneratorError
|
||||
}
|
||||
|
||||
if appSetGenerator.Plugin == nil {
|
||||
return nil, EmptyAppSetGeneratorError
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
providerConfig := appSetGenerator.Plugin
|
||||
|
||||
pluginClient, err := g.getPluginFromGenerator(ctx, applicationSetInfo.Name, providerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, err := pluginClient.List(ctx, providerConfig.Input.Parameters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error listing params: %w", err)
|
||||
}
|
||||
|
||||
res, err := g.generateParams(appSetGenerator, applicationSetInfo, list.Output.Parameters, appSetGenerator.Plugin.Input.Parameters, applicationSetInfo.Spec.GoTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) getPluginFromGenerator(ctx context.Context, appSetName string, generatorConfig *argoprojiov1alpha1.PluginGenerator) (*plugin.Service, error) {
|
||||
cm, err := g.getConfigMap(ctx, generatorConfig.ConfigMapRef.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching ConfigMap: %w", err)
|
||||
}
|
||||
token, err := g.getToken(ctx, cm["token"])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error fetching Secret token: %v", err)
|
||||
}
|
||||
|
||||
var requestTimeout int
|
||||
requestTimeoutStr, ok := cm["requestTimeout"]
|
||||
if ok {
|
||||
requestTimeout, err = strconv.Atoi(requestTimeoutStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error set requestTimeout : %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
pluginClient, err := plugin.NewPluginService(ctx, appSetName, cm["baseUrl"], token, requestTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pluginClient, nil
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) generateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, objectsFound []map[string]interface{}, pluginParams argoprojiov1alpha1.PluginParameters, useGoTemplate bool) ([]map[string]interface{}, error) {
|
||||
res := []map[string]interface{}{}
|
||||
|
||||
for _, objectFound := range objectsFound {
|
||||
|
||||
params := map[string]interface{}{}
|
||||
|
||||
if useGoTemplate {
|
||||
for k, v := range objectFound {
|
||||
params[k] = v
|
||||
}
|
||||
} else {
|
||||
flat, err := flatten.Flatten(objectFound, "", flatten.DotStyle)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range flat {
|
||||
params[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
params["generator"] = map[string]interface{}{
|
||||
"input": map[string]argoprojiov1alpha1.PluginParameters{
|
||||
"parameters": pluginParams,
|
||||
},
|
||||
}
|
||||
|
||||
err := appendTemplatedValues(appSetGenerator.Plugin.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = append(res, params)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) getToken(ctx context.Context, tokenRef string) (string, error) {
|
||||
|
||||
if tokenRef == "" || !strings.HasPrefix(tokenRef, "$") {
|
||||
return "", fmt.Errorf("token is empty, or does not reference a secret key starting with '$': %v", tokenRef)
|
||||
}
|
||||
|
||||
secretName, tokenKey := plugin.ParseSecretKey(tokenRef)
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
err := g.client.Get(
|
||||
ctx,
|
||||
client.ObjectKey{
|
||||
Name: secretName,
|
||||
Namespace: g.namespace,
|
||||
},
|
||||
secret)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching secret %s/%s: %v", g.namespace, secretName, err)
|
||||
}
|
||||
|
||||
secretValues := make(map[string]string, len(secret.Data))
|
||||
|
||||
for k, v := range secret.Data {
|
||||
secretValues[k] = string(v)
|
||||
}
|
||||
|
||||
token := settings.ReplaceStringSecret(tokenKey, secretValues)
|
||||
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (g *PluginGenerator) getConfigMap(ctx context.Context, configMapRef string) (map[string]string, error) {
|
||||
cm := &corev1.ConfigMap{}
|
||||
err := g.client.Get(
|
||||
ctx,
|
||||
client.ObjectKey{
|
||||
Name: configMapRef,
|
||||
Namespace: g.namespace,
|
||||
},
|
||||
cm)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseUrl, ok := cm.Data["baseUrl"]
|
||||
if !ok || baseUrl == "" {
|
||||
return nil, fmt.Errorf("baseUrl not found in ConfigMap")
|
||||
}
|
||||
|
||||
token, ok := cm.Data["token"]
|
||||
if !ok || token == "" {
|
||||
return nil, fmt.Errorf("token not found in ConfigMap")
|
||||
}
|
||||
|
||||
return cm.Data, nil
|
||||
}
|
||||
705
applicationset/generators/plugin_test.go
Normal file
705
applicationset/generators/plugin_test.go
Normal file
|
|
@ -0,0 +1,705 @@
|
|||
package generators
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/applicationset/services/plugin"
|
||||
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestPluginGenerateParams(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configmap *v1.ConfigMap
|
||||
secret *v1.Secret
|
||||
inputParameters map[string]apiextensionsv1.JSON
|
||||
values map[string]string
|
||||
gotemplate bool
|
||||
expected []map[string]interface{}
|
||||
content []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "simple case",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "simple case with values",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
values: map[string]string{
|
||||
"valuekey1": "valuevalue1",
|
||||
"valuekey2": "templated-{{key1}}",
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"values.valuekey1": "valuevalue1",
|
||||
"values.valuekey2": "templated-val1",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "simple case with gotemplate",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: true,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2": map[string]interface{}{
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": map[string]interface{}{
|
||||
"key2_2_1": "val2_2_1",
|
||||
},
|
||||
},
|
||||
"key3": float64(123),
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "simple case with appended params",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123,
|
||||
"pkey2": "valplugin"
|
||||
}]}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"pkey2": "valplugin",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "no params",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: argoprojiov1alpha1.PluginParameters{},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": map[string]map[string]interface{}{
|
||||
"parameters": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "empty return",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"input": {"parameters": []}}`),
|
||||
expected: []map[string]interface{}{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "wrong return",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{},
|
||||
gotemplate: false,
|
||||
content: []byte(`wrong body ...`),
|
||||
expected: []map[string]interface{}{},
|
||||
expectedError: fmt.Errorf("error listing params: error get api 'set': invalid character 'w' looking for beginning of value: wrong body ..."),
|
||||
},
|
||||
{
|
||||
name: "external secret",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin-secret:plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "plugin-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123,
|
||||
"pkey2": "valplugin"
|
||||
}]}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"pkey2": "valplugin",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "no secret",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: fmt.Errorf("error fetching Secret token: error fetching secret default/argocd-secret: secrets \"argocd-secret\" not found"),
|
||||
},
|
||||
{
|
||||
name: "no configmap",
|
||||
configmap: &v1.ConfigMap{},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: fmt.Errorf("error fetching ConfigMap: configmaps \"\" not found"),
|
||||
},
|
||||
{
|
||||
name: "no baseUrl",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"token": "$plugin.token",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"plugin.token": []byte("my-secret"),
|
||||
},
|
||||
},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: fmt.Errorf("error fetching ConfigMap: baseUrl not found in ConfigMap"),
|
||||
},
|
||||
{
|
||||
name: "no token",
|
||||
configmap: &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "first-plugin-cm",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"baseUrl": "http://127.0.0.1",
|
||||
},
|
||||
},
|
||||
secret: &v1.Secret{},
|
||||
inputParameters: map[string]apiextensionsv1.JSON{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
gotemplate: false,
|
||||
content: []byte(`{"output": {
|
||||
"parameters": [{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]
|
||||
}}`),
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2.key2_1": "val2_1",
|
||||
"key2.key2_2.key2_2_1": "val2_2_1",
|
||||
"key3": "123",
|
||||
"generator": map[string]interface{}{
|
||||
"input": argoprojiov1alpha1.PluginInput{
|
||||
Parameters: argoprojiov1alpha1.PluginParameters{
|
||||
"pkey1": {Raw: []byte(`"val1"`)},
|
||||
"pkey2": {Raw: []byte(`"val2"`)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: fmt.Errorf("error fetching ConfigMap: token not found in ConfigMap"),
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
|
||||
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
|
||||
Plugin: &argoprojiov1alpha1.PluginGenerator{
|
||||
ConfigMapRef: argoprojiov1alpha1.PluginConfigMapRef{Name: testCase.configmap.Name},
|
||||
Input: argoprojiov1alpha1.PluginInput{
|
||||
Parameters: testCase.inputParameters,
|
||||
},
|
||||
Values: testCase.values,
|
||||
},
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
_, tokenKey := plugin.ParseSecretKey(testCase.configmap.Data["token"])
|
||||
expectedToken := testCase.secret.Data[strings.Replace(tokenKey, "$", "", -1)]
|
||||
if authHeader != "Bearer "+string(expectedToken) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, err := w.Write(testCase.content)
|
||||
if err != nil {
|
||||
assert.NoError(t, fmt.Errorf("Error Write %v", err))
|
||||
}
|
||||
})
|
||||
|
||||
fakeServer := httptest.NewServer(handler)
|
||||
|
||||
defer fakeServer.Close()
|
||||
|
||||
if _, ok := testCase.configmap.Data["baseUrl"]; ok {
|
||||
testCase.configmap.Data["baseUrl"] = fakeServer.URL
|
||||
}
|
||||
|
||||
fakeClient := kubefake.NewSimpleClientset(append([]runtime.Object{}, testCase.configmap, testCase.secret)...)
|
||||
|
||||
fakeClientWithCache := fake.NewClientBuilder().WithObjects([]client.Object{testCase.configmap, testCase.secret}...).Build()
|
||||
|
||||
var pluginGenerator = NewPluginGenerator(fakeClientWithCache, ctx, fakeClient, "default")
|
||||
|
||||
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "set",
|
||||
},
|
||||
Spec: argoprojiov1alpha1.ApplicationSetSpec{
|
||||
GoTemplate: testCase.gotemplate,
|
||||
},
|
||||
}
|
||||
|
||||
got, err := pluginGenerator.GenerateParams(&generatorConfig, &applicationSetInfo)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
if testCase.expectedError != nil {
|
||||
assert.EqualError(t, err, testCase.expectedError.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
expectedJson, err := json.Marshal(testCase.expected)
|
||||
require.NoError(t, err)
|
||||
gotJson, err := json.Marshal(got)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(expectedJson), string(gotJson))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
161
applicationset/services/internal/http/client.go
Normal file
161
applicationset/services/internal/http/client.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
userAgent = "argocd-applicationset"
|
||||
defaultTimeout = 30
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
// URL is the URL used for API requests.
|
||||
baseURL string
|
||||
|
||||
// UserAgent is the user agent to include in HTTP requests.
|
||||
UserAgent string
|
||||
|
||||
// Token is used to make authenticated API calls.
|
||||
token string
|
||||
|
||||
// Client is an HTTP client used to communicate with the API.
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Body []byte
|
||||
Response *http.Response
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewClient(baseURL string, options ...ClientOptionFunc) (*Client, error) {
|
||||
client, err := newClient(baseURL, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func newClient(baseURL string, options ...ClientOptionFunc) (*Client, error) {
|
||||
c := &Client{baseURL: baseURL, UserAgent: userAgent}
|
||||
|
||||
// Configure the HTTP client.
|
||||
c.client = &http.Client{
|
||||
Timeout: time.Duration(defaultTimeout) * time.Second,
|
||||
}
|
||||
|
||||
// Apply any given client options.
|
||||
for _, fn := range options {
|
||||
if fn == nil {
|
||||
continue
|
||||
}
|
||||
if err := fn(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Client) NewRequest(method, path string, body interface{}, options []ClientOptionFunc) (*http.Request, error) {
|
||||
|
||||
// Make sure the given URL end with a slash
|
||||
if !strings.HasSuffix(c.baseURL, "/") {
|
||||
c.baseURL += "/"
|
||||
}
|
||||
|
||||
var buf io.ReadWriter
|
||||
if body != nil {
|
||||
buf = &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
err := enc.Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, c.baseURL+path, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
if len(c.token) != 0 {
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
}
|
||||
|
||||
if c.UserAgent != "" {
|
||||
req.Header.Set("User-Agent", c.UserAgent)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := CheckResponse(resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
case io.Writer:
|
||||
_, err = io.Copy(v, resp.Body)
|
||||
default:
|
||||
buf := new(bytes.Buffer)
|
||||
teeReader := io.TeeReader(resp.Body, buf)
|
||||
decErr := json.NewDecoder(teeReader).Decode(v)
|
||||
if decErr == io.EOF {
|
||||
decErr = nil // ignore EOF errors caused by empty response body
|
||||
}
|
||||
if decErr != nil {
|
||||
err = fmt.Errorf("%s: %s", decErr.Error(), buf.String())
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// CheckResponse checks the API response for errors, and returns them if present.
|
||||
func CheckResponse(resp *http.Response) error {
|
||||
|
||||
if c := resp.StatusCode; 200 <= c && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("API error with status code %d: %v", resp.StatusCode, err)
|
||||
}
|
||||
|
||||
var raw map[string]interface{}
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, string(data))
|
||||
}
|
||||
|
||||
message := ""
|
||||
if value, ok := raw["message"].(string); ok {
|
||||
message = value
|
||||
} else if value, ok := raw["error"].(string); ok {
|
||||
message = value
|
||||
}
|
||||
|
||||
return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, message)
|
||||
}
|
||||
22
applicationset/services/internal/http/client_options.go
Normal file
22
applicationset/services/internal/http/client_options.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package http
|
||||
|
||||
import "time"
|
||||
|
||||
// ClientOptionFunc can be used to customize a new Restful API client.
|
||||
type ClientOptionFunc func(*Client) error
|
||||
|
||||
// WithToken is an option for NewClient to set token
|
||||
func WithToken(token string) ClientOptionFunc {
|
||||
return func(c *Client) error {
|
||||
c.token = token
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout can be used to configure a custom timeout for requests.
|
||||
func WithTimeout(timeout int) ClientOptionFunc {
|
||||
return func(c *Client) error {
|
||||
c.client.Timeout = time.Duration(timeout) * time.Second
|
||||
return nil
|
||||
}
|
||||
}
|
||||
163
applicationset/services/internal/http/client_test.go
Normal file
163
applicationset/services/internal/http/client_test.go
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := w.Write([]byte("Hello, World!"))
|
||||
if err != nil {
|
||||
assert.NoError(t, fmt.Errorf("Error Write %v", err))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
var clientOptionFns []ClientOptionFunc
|
||||
_, err := NewClient(server.URL, clientOptionFns...)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientDo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
params map[string]string
|
||||
content []byte
|
||||
fakeServer *httptest.Server
|
||||
clientOptionFns []ClientOptionFunc
|
||||
expected []map[string]interface{}
|
||||
expectedCode int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Simple",
|
||||
params: map[string]string{
|
||||
"pkey1": "val1",
|
||||
"pkey2": "val2",
|
||||
},
|
||||
fakeServer: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := w.Write([]byte(`[{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]`))
|
||||
if err != nil {
|
||||
assert.NoError(t, fmt.Errorf("Error Write %v", err))
|
||||
}
|
||||
})),
|
||||
clientOptionFns: nil,
|
||||
expected: []map[string]interface{}{
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2": map[string]interface{}{
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": map[string]interface{}{
|
||||
"key2_2_1": "val2_2_1",
|
||||
},
|
||||
},
|
||||
"key3": float64(123),
|
||||
},
|
||||
},
|
||||
expectedCode: 200,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "With Token",
|
||||
params: map[string]string{
|
||||
"pkey1": "val1",
|
||||
"pkey2": "val2",
|
||||
},
|
||||
fakeServer: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer "+string("test-token") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := w.Write([]byte(`[{
|
||||
"key1": "val1",
|
||||
"key2": {
|
||||
"key2_1": "val2_1",
|
||||
"key2_2": {
|
||||
"key2_2_1": "val2_2_1"
|
||||
}
|
||||
},
|
||||
"key3": 123
|
||||
}]`))
|
||||
if err != nil {
|
||||
assert.NoError(t, fmt.Errorf("Error Write %v", err))
|
||||
}
|
||||
})),
|
||||
clientOptionFns: nil,
|
||||
expected: []map[string]interface{}(nil),
|
||||
expectedCode: 401,
|
||||
expectedError: fmt.Errorf("API error with status code 401: "),
|
||||
},
|
||||
} {
|
||||
cc := c
|
||||
t.Run(cc.name, func(t *testing.T) {
|
||||
defer cc.fakeServer.Close()
|
||||
|
||||
client, err := NewClient(cc.fakeServer.URL, cc.clientOptionFns...)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
req, err := client.NewRequest("POST", "", cc.params, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var data []map[string]interface{}
|
||||
|
||||
resp, err := client.Do(ctx, req, &data)
|
||||
|
||||
if cc.expectedError != nil {
|
||||
assert.EqualError(t, err, cc.expectedError.Error())
|
||||
} else {
|
||||
assert.Equal(t, resp.StatusCode, cc.expectedCode)
|
||||
assert.Equal(t, data, cc.expected)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResponse(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"error":"invalid_request","description":"Invalid token"}`)),
|
||||
}
|
||||
|
||||
err := CheckResponse(resp)
|
||||
if err == nil {
|
||||
t.Error("Expected an error, got nil")
|
||||
}
|
||||
|
||||
expected := "API error with status code 400: invalid_request"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected error '%s', got '%s'", expected, err.Error())
|
||||
}
|
||||
}
|
||||
73
applicationset/services/plugin/plugin_service.go
Normal file
73
applicationset/services/plugin/plugin_service.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
internalhttp "github.com/argoproj/argo-cd/v2/applicationset/services/internal/http"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// ServiceRequest is the request object sent to the plugin service.
|
||||
type ServiceRequest struct {
|
||||
// ApplicationSetName is the appSetName of the ApplicationSet for which we're requesting parameters. Useful for logging in
|
||||
// the plugin service.
|
||||
ApplicationSetName string `json:"applicationSetName"`
|
||||
// Input is the map of parameters set in the ApplicationSet spec for this generator.
|
||||
Input v1alpha1.PluginInput `json:"input"`
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
// Parameters is the list of parameter sets returned by the plugin.
|
||||
Parameters []map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
// ServiceResponse is the response object returned by the plugin service.
|
||||
type ServiceResponse struct {
|
||||
// Output is the map of outputs returned by the plugin.
|
||||
Output Output `json:"output"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
client *internalhttp.Client
|
||||
appSetName string
|
||||
}
|
||||
|
||||
func NewPluginService(ctx context.Context, appSetName string, baseURL string, token string, requestTimeout int) (*Service, error) {
|
||||
var clientOptionFns []internalhttp.ClientOptionFunc
|
||||
|
||||
clientOptionFns = append(clientOptionFns, internalhttp.WithToken(token))
|
||||
|
||||
if requestTimeout != 0 {
|
||||
clientOptionFns = append(clientOptionFns, internalhttp.WithTimeout(requestTimeout))
|
||||
}
|
||||
|
||||
client, err := internalhttp.NewClient(baseURL, clientOptionFns...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating plugin client: %v", err)
|
||||
}
|
||||
|
||||
return &Service{
|
||||
client: client,
|
||||
appSetName: appSetName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Service) List(ctx context.Context, parameters v1alpha1.PluginParameters) (*ServiceResponse, error) {
|
||||
req, err := p.client.NewRequest(http.MethodPost, "api/v1/getparams.execute", ServiceRequest{ApplicationSetName: p.appSetName, Input: v1alpha1.PluginInput{Parameters: parameters}}, nil)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewRequest returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var data ServiceResponse
|
||||
|
||||
_, err = p.client.Do(ctx, req, &data)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error get api '%s': %v", p.appSetName, err)
|
||||
}
|
||||
|
||||
return &data, err
|
||||
}
|
||||
52
applicationset/services/plugin/plugin_service_test.go
Normal file
52
applicationset/services/plugin/plugin_service_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlugin(t *testing.T) {
|
||||
expectedJSON := `{"parameters": [{"number":123,"digest":"sha256:942ae2dfd73088b54d7151a3c3fd5af038a51c50029bfcfd21f1e650d9579967"},{"number":456,"digest":"sha256:224e68cc69566e5cbbb76034b3c42cd2ed57c1a66720396e1c257794cb7d68c1"}]}`
|
||||
token := "0bc57212c3cbbec69d20b34c507284bd300def5b"
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader != "Bearer "+token {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
_, err := w.Write([]byte(expectedJSON))
|
||||
|
||||
if err != nil {
|
||||
assert.NoError(t, fmt.Errorf("Error Write %v", err))
|
||||
}
|
||||
})
|
||||
ts := httptest.NewServer(handler)
|
||||
defer ts.Close()
|
||||
|
||||
client, err := NewPluginService(context.Background(), "plugin-test", ts.URL, token, 0)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data, err := client.List(context.Background(), nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var expectedData ServiceResponse
|
||||
err = json.Unmarshal([]byte(expectedJSON), &expectedData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, &expectedData, data)
|
||||
}
|
||||
21
applicationset/services/plugin/utils.go
Normal file
21
applicationset/services/plugin/utils.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
)
|
||||
|
||||
// ParseSecretKey retrieves secret appSetName if different from common ArgoCDSecretName.
|
||||
func ParseSecretKey(key string) (secretName string, tokenKey string) {
|
||||
if strings.Contains(key, ":") {
|
||||
parts := strings.Split(key, ":")
|
||||
secretName = parts[0][1:]
|
||||
tokenKey = fmt.Sprintf("$%s", parts[1])
|
||||
} else {
|
||||
secretName = common.ArgoCDSecretName
|
||||
tokenKey = key
|
||||
}
|
||||
return secretName, tokenKey
|
||||
}
|
||||
17
applicationset/services/plugin/utils_test.go
Normal file
17
applicationset/services/plugin/utils_test.go
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseSecretKey(t *testing.T) {
|
||||
secretName, tokenKey := ParseSecretKey("#my-secret:my-token")
|
||||
assert.Equal(t, "my-secret", secretName)
|
||||
assert.Equal(t, "$my-token", tokenKey)
|
||||
|
||||
secretName, tokenKey = ParseSecretKey("#my-secret")
|
||||
assert.Equal(t, "argocd-secret", secretName)
|
||||
assert.Equal(t, "#my-secret", tokenKey)
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[stri
|
|||
}
|
||||
for _, key := range original.MapKeys() {
|
||||
originalValue := original.MapIndex(key)
|
||||
if originalValue.Kind() != reflect.String && originalValue.IsNil() {
|
||||
if originalValue.Kind() != reflect.String && isNillable(originalValue) && originalValue.IsNil() {
|
||||
continue
|
||||
}
|
||||
// New gives us a pointer, but again we want the value
|
||||
|
|
@ -191,6 +191,16 @@ func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[stri
|
|||
return nil
|
||||
}
|
||||
|
||||
// isNillable returns true if the value is something which may be set to nil. This function is meant to guard against a
|
||||
// panic from calling IsNil on a non-pointer type.
|
||||
func isNillable(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error) {
|
||||
if tmpl == nil {
|
||||
return nil, fmt.Errorf("application template is empty")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
logtest "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
|
@ -484,6 +485,34 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRenderGeneratorParams_does_not_panic(t *testing.T) {
|
||||
// This test verifies that the RenderGeneratorParams function does not panic when the value in a map is a non-
|
||||
// nillable type. This is a regression test.
|
||||
render := Render{}
|
||||
params := map[string]interface{}{
|
||||
"branch": "master",
|
||||
}
|
||||
generator := &argoappsv1.ApplicationSetGenerator{
|
||||
Plugin: &argoappsv1.PluginGenerator{
|
||||
ConfigMapRef: argoappsv1.PluginConfigMapRef{
|
||||
Name: "cm-plugin",
|
||||
},
|
||||
Input: argoappsv1.PluginInput{
|
||||
Parameters: map[string]apiextensionsv1.JSON{
|
||||
"branch": {
|
||||
Raw: []byte(`"{{.branch}}"`),
|
||||
},
|
||||
"repo": {
|
||||
Raw: []byte(`"argo-test"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := render.RenderGeneratorParams(generator, params, true, []string{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRenderTemplateKeys(t *testing.T) {
|
||||
t.Run("fasttemplate", func(t *testing.T) {
|
||||
application := &argoappsv1.Application{
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ func (h *WebhookHandler) HandleEvent(payload interface{}) {
|
|||
// check if the ApplicationSet uses any generator that is relevant to the payload
|
||||
shouldRefresh = shouldRefreshGitGenerator(gen.Git, gitGenInfo) ||
|
||||
shouldRefreshPRGenerator(gen.PullRequest, prGenInfo) ||
|
||||
shouldRefreshPluginGenerator(gen.Plugin) ||
|
||||
h.shouldRefreshMatrixGenerator(gen.Matrix, &appSet, gitGenInfo, prGenInfo) ||
|
||||
h.shouldRefreshMergeGenerator(gen.Merge, &appSet, gitGenInfo, prGenInfo)
|
||||
if shouldRefresh {
|
||||
|
|
@ -287,6 +288,10 @@ func shouldRefreshGitGenerator(gen *v1alpha1.GitGenerator, info *gitGeneratorInf
|
|||
return true
|
||||
}
|
||||
|
||||
func shouldRefreshPluginGenerator(gen *v1alpha1.PluginGenerator) bool {
|
||||
return gen != nil
|
||||
}
|
||||
|
||||
func genRevisionHasChanged(gen *v1alpha1.GitGenerator, revision string, touchedHead bool) bool {
|
||||
targetRev := parseRevision(gen.Revision)
|
||||
if targetRev == "HEAD" || targetRev == "" { // revision is head
|
||||
|
|
@ -417,6 +422,7 @@ func (h *WebhookHandler) shouldRefreshMatrixGenerator(gen *v1alpha1.MatrixGenera
|
|||
SCMProvider: g0.SCMProvider,
|
||||
ClusterDecisionResource: g0.ClusterDecisionResource,
|
||||
PullRequest: g0.PullRequest,
|
||||
Plugin: g0.Plugin,
|
||||
Matrix: matrixGenerator0,
|
||||
Merge: mergeGenerator0,
|
||||
}
|
||||
|
|
@ -471,6 +477,7 @@ func (h *WebhookHandler) shouldRefreshMatrixGenerator(gen *v1alpha1.MatrixGenera
|
|||
SCMProvider: g1.SCMProvider,
|
||||
ClusterDecisionResource: g1.ClusterDecisionResource,
|
||||
PullRequest: g1.PullRequest,
|
||||
Plugin: g1.Plugin,
|
||||
Matrix: matrixGenerator1,
|
||||
Merge: mergeGenerator1,
|
||||
}
|
||||
|
|
@ -488,6 +495,7 @@ func (h *WebhookHandler) shouldRefreshMatrixGenerator(gen *v1alpha1.MatrixGenera
|
|||
// Check all interpolated child generators
|
||||
if shouldRefreshGitGenerator(interpolatedGenerator.Git, gitGenInfo) ||
|
||||
shouldRefreshPRGenerator(interpolatedGenerator.PullRequest, prGenInfo) ||
|
||||
shouldRefreshPluginGenerator(interpolatedGenerator.Plugin) ||
|
||||
h.shouldRefreshMatrixGenerator(interpolatedGenerator.Matrix, appSet, gitGenInfo, prGenInfo) ||
|
||||
h.shouldRefreshMergeGenerator(requestedGenerator1.Merge, appSet, gitGenInfo, prGenInfo) {
|
||||
return true
|
||||
|
|
@ -498,6 +506,7 @@ func (h *WebhookHandler) shouldRefreshMatrixGenerator(gen *v1alpha1.MatrixGenera
|
|||
// First child generator didn't return any params, just check the second child generator
|
||||
return shouldRefreshGitGenerator(requestedGenerator1.Git, gitGenInfo) ||
|
||||
shouldRefreshPRGenerator(requestedGenerator1.PullRequest, prGenInfo) ||
|
||||
shouldRefreshPluginGenerator(requestedGenerator1.Plugin) ||
|
||||
h.shouldRefreshMatrixGenerator(requestedGenerator1.Matrix, appSet, gitGenInfo, prGenInfo) ||
|
||||
h.shouldRefreshMergeGenerator(requestedGenerator1.Merge, appSet, gitGenInfo, prGenInfo)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-GitHub-Event",
|
||||
headerValue: "push",
|
||||
payloadFile: "github-commit-event.json",
|
||||
effectedAppSets: []string{"git-github", "matrix-git-github", "merge-git-github", "matrix-scm-git-github", "matrix-nested-git-github", "merge-nested-git-github"},
|
||||
effectedAppSets: []string{"git-github", "matrix-git-github", "merge-git-github", "matrix-scm-git-github", "matrix-nested-git-github", "merge-nested-git-github", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: true,
|
||||
},
|
||||
|
|
@ -69,7 +69,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-GitHub-Event",
|
||||
headerValue: "push",
|
||||
payloadFile: "github-commit-branch-event.json",
|
||||
effectedAppSets: []string{"git-github"},
|
||||
effectedAppSets: []string{"git-github", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: true,
|
||||
},
|
||||
|
|
@ -78,7 +78,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-GitHub-Event",
|
||||
headerValue: "ping",
|
||||
payloadFile: "github-ping-event.json",
|
||||
effectedAppSets: []string{"git-github"},
|
||||
effectedAppSets: []string{"git-github", "plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: false,
|
||||
},
|
||||
|
|
@ -87,7 +87,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-Gitlab-Event",
|
||||
headerValue: "Push Hook",
|
||||
payloadFile: "gitlab-event.json",
|
||||
effectedAppSets: []string{"git-gitlab"},
|
||||
effectedAppSets: []string{"git-gitlab", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: true,
|
||||
},
|
||||
|
|
@ -96,7 +96,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-Random-Event",
|
||||
headerValue: "Push Hook",
|
||||
payloadFile: "gitlab-event.json",
|
||||
effectedAppSets: []string{"git-gitlab"},
|
||||
effectedAppSets: []string{"git-gitlab", "plugin"},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
expectedRefresh: false,
|
||||
},
|
||||
|
|
@ -105,7 +105,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-Random-Event",
|
||||
headerValue: "Push Hook",
|
||||
payloadFile: "invalid-event.json",
|
||||
effectedAppSets: []string{"git-gitlab"},
|
||||
effectedAppSets: []string{"git-gitlab", "plugin"},
|
||||
expectedStatusCode: http.StatusBadRequest,
|
||||
expectedRefresh: false,
|
||||
},
|
||||
|
|
@ -114,7 +114,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-GitHub-Event",
|
||||
headerValue: "pull_request",
|
||||
payloadFile: "github-pull-request-opened-event.json",
|
||||
effectedAppSets: []string{"pull-request-github", "matrix-pull-request-github", "matrix-scm-pull-request-github", "merge-pull-request-github"},
|
||||
effectedAppSets: []string{"pull-request-github", "matrix-pull-request-github", "matrix-scm-pull-request-github", "merge-pull-request-github", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: true,
|
||||
},
|
||||
|
|
@ -123,7 +123,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-GitHub-Event",
|
||||
headerValue: "pull_request",
|
||||
payloadFile: "github-pull-request-assigned-event.json",
|
||||
effectedAppSets: []string{"pull-request-github", "matrix-pull-request-github", "matrix-scm-pull-request-github", "merge-pull-request-github"},
|
||||
effectedAppSets: []string{"pull-request-github", "matrix-pull-request-github", "matrix-scm-pull-request-github", "merge-pull-request-github", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: false,
|
||||
},
|
||||
|
|
@ -132,7 +132,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-Gitlab-Event",
|
||||
headerValue: "Merge Request Hook",
|
||||
payloadFile: "gitlab-merge-request-open-event.json",
|
||||
effectedAppSets: []string{"pull-request-gitlab"},
|
||||
effectedAppSets: []string{"pull-request-gitlab", "plugin", "matrix-pull-request-github-plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: true,
|
||||
},
|
||||
|
|
@ -141,7 +141,7 @@ func TestWebhookHandler(t *testing.T) {
|
|||
headerKey: "X-Gitlab-Event",
|
||||
headerValue: "Merge Request Hook",
|
||||
payloadFile: "gitlab-merge-request-approval-event.json",
|
||||
effectedAppSets: []string{"pull-request-gitlab"},
|
||||
effectedAppSets: []string{"pull-request-gitlab", "plugin"},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedRefresh: false,
|
||||
},
|
||||
|
|
@ -162,11 +162,13 @@ func TestWebhookHandler(t *testing.T) {
|
|||
fakeAppWithGitGenerator("git-gitlab", namespace, "https://gitlab/group/name"),
|
||||
fakeAppWithGithubPullRequestGenerator("pull-request-github", namespace, "Codertocat", "Hello-World"),
|
||||
fakeAppWithGitlabPullRequestGenerator("pull-request-gitlab", namespace, "100500"),
|
||||
fakeAppWithPluginGenerator("plugin", namespace),
|
||||
fakeAppWithMatrixAndGitGenerator("matrix-git-github", namespace, "https://github.com/org/repo"),
|
||||
fakeAppWithMatrixAndPullRequestGenerator("matrix-pull-request-github", namespace, "Codertocat", "Hello-World"),
|
||||
fakeAppWithMatrixAndScmWithGitGenerator("matrix-scm-git-github", namespace, "org"),
|
||||
fakeAppWithMatrixAndScmWithPullRequestGenerator("matrix-scm-pull-request-github", namespace, "Codertocat"),
|
||||
fakeAppWithMatrixAndNestedGitGenerator("matrix-nested-git-github", namespace, "https://github.com/org/repo"),
|
||||
fakeAppWithMatrixAndPullRequestGeneratorWithPluginGenerator("matrix-pull-request-github-plugin", namespace, "Codertocat", "Hello-World", "plugin-cm"),
|
||||
fakeAppWithMergeAndGitGenerator("merge-git-github", namespace, "https://github.com/org/repo"),
|
||||
fakeAppWithMergeAndPullRequestGenerator("merge-pull-request-github", namespace, "Codertocat", "Hello-World"),
|
||||
fakeAppWithMergeAndNestedGitGenerator("merge-nested-git-github", namespace, "https://github.com/org/repo"),
|
||||
|
|
@ -214,6 +216,7 @@ func mockGenerators() map[string]generators.Generator {
|
|||
// generatorMockList := generatorMock{}
|
||||
generatorMockGit := &generatorMock{}
|
||||
generatorMockPR := &generatorMock{}
|
||||
generatorMockPlugin := &generatorMock{}
|
||||
mockSCMProvider := &scm_provider.MockProvider{
|
||||
Repos: []*scm_provider.Repository{
|
||||
{
|
||||
|
|
@ -239,6 +242,7 @@ func mockGenerators() map[string]generators.Generator {
|
|||
"Git": generatorMockGit,
|
||||
"SCMProvider": generatorMockSCM,
|
||||
"PullRequest": generatorMockPR,
|
||||
"Plugin": generatorMockPlugin,
|
||||
}
|
||||
|
||||
nestedGenerators := map[string]generators.Generator{
|
||||
|
|
@ -246,6 +250,7 @@ func mockGenerators() map[string]generators.Generator {
|
|||
"Git": terminalMockGenerators["Git"],
|
||||
"SCMProvider": terminalMockGenerators["SCMProvider"],
|
||||
"PullRequest": terminalMockGenerators["PullRequest"],
|
||||
"Plugin": terminalMockGenerators["Plugin"],
|
||||
"Matrix": generators.NewMatrixGenerator(terminalMockGenerators),
|
||||
"Merge": generators.NewMergeGenerator(terminalMockGenerators),
|
||||
}
|
||||
|
|
@ -255,6 +260,7 @@ func mockGenerators() map[string]generators.Generator {
|
|||
"Git": terminalMockGenerators["Git"],
|
||||
"SCMProvider": terminalMockGenerators["SCMProvider"],
|
||||
"PullRequest": terminalMockGenerators["PullRequest"],
|
||||
"Plugin": terminalMockGenerators["Plugin"],
|
||||
"Matrix": generators.NewMatrixGenerator(nestedGenerators),
|
||||
"Merge": generators.NewMergeGenerator(nestedGenerators),
|
||||
}
|
||||
|
|
@ -592,6 +598,60 @@ func fakeAppWithMergeAndNestedGitGenerator(name, namespace, repo string) *v1alph
|
|||
}
|
||||
}
|
||||
|
||||
func fakeAppWithPluginGenerator(name, namespace string) *v1alpha1.ApplicationSet {
|
||||
return &v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Plugin: &v1alpha1.PluginGenerator{
|
||||
ConfigMapRef: v1alpha1.PluginConfigMapRef{
|
||||
Name: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fakeAppWithMatrixAndPullRequestGeneratorWithPluginGenerator(name, namespace, owner, repo, configmapName string) *v1alpha1.ApplicationSet {
|
||||
return &v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Matrix: &v1alpha1.MatrixGenerator{
|
||||
Generators: []v1alpha1.ApplicationSetNestedGenerator{
|
||||
{
|
||||
PullRequest: &v1alpha1.PullRequestGenerator{
|
||||
Github: &v1alpha1.PullRequestGeneratorGithub{
|
||||
Owner: owner,
|
||||
Repo: repo,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Plugin: &v1alpha1.PluginGenerator{
|
||||
ConfigMapRef: v1alpha1.PluginConfigMapRef{
|
||||
Name: configmapName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeClient(ns string) *kubefake.Clientset {
|
||||
s := runtime.NewScheme()
|
||||
s.AddKnownTypes(v1alpha1.SchemeGroupVersion, &v1alpha1.ApplicationSet{})
|
||||
|
|
|
|||
|
|
@ -5848,6 +5848,9 @@
|
|||
"merge": {
|
||||
"$ref": "#/definitions/v1alpha1MergeGenerator"
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/v1alpha1PluginGenerator"
|
||||
},
|
||||
"pullRequest": {
|
||||
"$ref": "#/definitions/v1alpha1PullRequestGenerator"
|
||||
},
|
||||
|
|
@ -5896,6 +5899,9 @@
|
|||
"merge": {
|
||||
"$ref": "#/definitions/v1JSON"
|
||||
},
|
||||
"plugin": {
|
||||
"$ref": "#/definitions/v1alpha1PluginGenerator"
|
||||
},
|
||||
"pullRequest": {
|
||||
"$ref": "#/definitions/v1alpha1PullRequestGenerator"
|
||||
},
|
||||
|
|
@ -7313,6 +7319,54 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1PluginConfigMapRef": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "Name of the ConfigMap"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1PluginGenerator": {
|
||||
"description": "PluginGenerator defines connection info specific to Plugin.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configMapRef": {
|
||||
"$ref": "#/definitions/v1alpha1PluginConfigMapRef"
|
||||
},
|
||||
"input": {
|
||||
"$ref": "#/definitions/v1alpha1PluginInput"
|
||||
},
|
||||
"requeueAfterSeconds": {
|
||||
"description": "RequeueAfterSeconds determines how long the ApplicationSet controller will wait before reconciling the ApplicationSet again.",
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
},
|
||||
"template": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSetTemplate"
|
||||
},
|
||||
"values": {
|
||||
"description": "Values contains key/value pairs which are passed directly as parameters to the template. These values will not be\nsent as parameters to the plugin.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1PluginInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parameters": {
|
||||
"description": "Parameters contains the information to pass to the plugin. It is a map. The keys must be strings, and the\nvalues can be any type.",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/v1JSON"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ProjectRole": {
|
||||
"type": "object",
|
||||
"title": "ProjectRole represents a role that has access to a project",
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ func NewCommand() *cobra.Command {
|
|||
"SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient(), scmAuth),
|
||||
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace),
|
||||
"PullRequest": generators.NewPullRequestGenerator(mgr.GetClient(), scmAuth),
|
||||
"Plugin": generators.NewPluginGenerator(mgr.GetClient(), ctx, k8sClient, namespace),
|
||||
}
|
||||
|
||||
nestedGenerators := map[string]generators.Generator{
|
||||
|
|
@ -161,6 +162,7 @@ func NewCommand() *cobra.Command {
|
|||
"SCMProvider": terminalGenerators["SCMProvider"],
|
||||
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
|
||||
"PullRequest": terminalGenerators["PullRequest"],
|
||||
"Plugin": terminalGenerators["Plugin"],
|
||||
"Matrix": generators.NewMatrixGenerator(terminalGenerators),
|
||||
"Merge": generators.NewMergeGenerator(terminalGenerators),
|
||||
}
|
||||
|
|
@ -172,6 +174,7 @@ func NewCommand() *cobra.Command {
|
|||
"SCMProvider": terminalGenerators["SCMProvider"],
|
||||
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
|
||||
"PullRequest": terminalGenerators["PullRequest"],
|
||||
"Plugin": terminalGenerators["Plugin"],
|
||||
"Matrix": generators.NewMatrixGenerator(nestedGenerators),
|
||||
"Merge": generators.NewMergeGenerator(nestedGenerators),
|
||||
}
|
||||
|
|
|
|||
341
docs/operator-manual/applicationset/Generators-Plugin.md
Normal file
341
docs/operator-manual/applicationset/Generators-Plugin.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Plugin Generator
|
||||
|
||||
Plugins allow you to provide your own generator.
|
||||
|
||||
- You can write in any language
|
||||
- Simple: a plugin just responds to RPC HTTP requests.
|
||||
- You can use it in a sidecar, or standalone deployment.
|
||||
- You can get your plugin running today, no need to wait 3-5 months for review, approval, merge and an Argo software
|
||||
release.
|
||||
- You can combine it with Matrix or Merge.
|
||||
|
||||
To start working on your own plugin, you can generate a new repository based on the example
|
||||
[applicationset-hello-plugin](https://github.com/argoproj-labs/applicationset-hello-plugin).
|
||||
|
||||
## Simple example
|
||||
|
||||
Using a generator plugin without combining it with Matrix or Merge.
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
name: myplugin
|
||||
spec:
|
||||
generators:
|
||||
- plugin:
|
||||
# Specify the configMap where the plugin configuration is located.
|
||||
configMapRef:
|
||||
name: my-plugin
|
||||
# You can pass arbitrary parameters to the plugin. `input.parameters` is a map, but values may be any type.
|
||||
# These parameters will also be available on the generator's output under the `generator.input.parameters` key.
|
||||
input:
|
||||
parameters:
|
||||
key1: "value1"
|
||||
key2: "value2"
|
||||
list: ["list", "of", "values"]
|
||||
boolean: true
|
||||
map:
|
||||
key1: "value1"
|
||||
key2: "value2"
|
||||
key3: "value3"
|
||||
|
||||
# You can also attach arbitrary values to the generator's output under the `values` key. These values will be
|
||||
# available in templates under the `values` key.
|
||||
values:
|
||||
value1: something
|
||||
|
||||
# When using a Plugin generator, the ApplicationSet controller polls every `requeueAfterSeconds` interval (defaulting to every 30 minutes) to detect changes.
|
||||
requeueAfterSeconds: 30
|
||||
template:
|
||||
metadata:
|
||||
name: myplugin
|
||||
annotations:
|
||||
example.from.input.parameters: "{{ generator.input.parameters.map.key1 }}"
|
||||
example.from.values: "{{ values.value1 }}"
|
||||
# The plugin determines what else it produces.
|
||||
example.from.plugin.output: "{{ something.from.the.plugin }}"
|
||||
```
|
||||
|
||||
- `configMapRef.name`: A `ConfigMap` name containing the plugin configuration to use for RPC call.
|
||||
- `input.parameters`: Input parameters included in the RPC call to the plugin. (Optional)
|
||||
|
||||
!!! note
|
||||
The concept of the plugin should not undermine the spirit of GitOps by externalizing data outside of Git. The goal is to be complementary in specific contexts.
|
||||
For example, when using one of the PullRequest generators, it's impossible to retrieve parameters related to the CI (only the commit hash is available), which limits the possibilities. By using a plugin, it's possible to retrieve the necessary parameters from a separate data source and use them to extend the functionality of the generator.
|
||||
|
||||
### Add a ConfigMap to configure the access of the plugin
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: my-plugin
|
||||
namespace: argocd
|
||||
data:
|
||||
token: "$plugin.myplugin.token" # Alternatively $<some_K8S_secret>:plugin.myplugin.token
|
||||
baseUrl: "http://myplugin.plugin-ns.svc.cluster.local."
|
||||
```
|
||||
|
||||
- `token`: Pre-shared token used to authenticate HTTP request (points to the right key you created in the `argocd-secret` Secret)
|
||||
- `baseUrl`: BaseUrl of the k8s service exposing your plugin in the cluster.
|
||||
|
||||
### Store credentials
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-secret
|
||||
namespace: argocd
|
||||
labels:
|
||||
app.kubernetes.io/name: argocd-secret
|
||||
app.kubernetes.io/part-of: argocd
|
||||
type: Opaque
|
||||
data:
|
||||
# ...
|
||||
# The secret value must be base64 encoded **once**
|
||||
# this value corresponds to: `printf "strong-password" | base64`
|
||||
plugin.myplugin.token: "c3Ryb25nLXBhc3N3b3Jk"
|
||||
# ...
|
||||
```
|
||||
|
||||
#### Alternative
|
||||
|
||||
If you want to store sensitive data in **another** Kubernetes `Secret`, instead of `argocd-secret`, ArgoCD knows how to check the keys under `data` in your Kubernetes `Secret` for a corresponding key whenever a value in a configmap starts with `$`, then your Kubernetes `Secret` name and `:` (colon) followed by the key name.
|
||||
|
||||
Syntax: `$<k8s_secret_name>:<a_key_in_that_k8s_secret>`
|
||||
|
||||
> NOTE: Secret must have label `app.kubernetes.io/part-of: argocd`
|
||||
|
||||
##### Example
|
||||
|
||||
`another-secret`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: another-secret
|
||||
namespace: argocd
|
||||
labels:
|
||||
app.kubernetes.io/part-of: argocd
|
||||
type: Opaque
|
||||
data:
|
||||
# ...
|
||||
# Store client secret like below.
|
||||
# Ensure the secret is base64 encoded
|
||||
plugin.myplugin.token: <client-secret-base64-encoded>
|
||||
# ...
|
||||
```
|
||||
|
||||
### HTTP server
|
||||
|
||||
#### A Simple Python Plugin
|
||||
|
||||
You can deploy it either as a sidecar or as a standalone deployment (the latter is recommended).
|
||||
|
||||
In the example, the token is stored in a file at this location : `/var/run/argo/token`
|
||||
|
||||
```
|
||||
string-password
|
||||
```
|
||||
|
||||
```python
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
with open("/var/run/argo/token") as f:
|
||||
plugin_token = f.read().strip()
|
||||
|
||||
|
||||
class Plugin(BaseHTTPRequestHandler):
|
||||
|
||||
def args(self):
|
||||
return json.loads(self.rfile.read(int(self.headers.get('Content-Length'))))
|
||||
|
||||
def reply(self, reply):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(reply).encode("UTF-8"))
|
||||
|
||||
def forbidden(self):
|
||||
self.send_response(403)
|
||||
self.end_headers()
|
||||
|
||||
def unsupported(self):
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self):
|
||||
if self.headers.get("Authorization") != "Bearer " + plugin_token:
|
||||
self.forbidden()
|
||||
|
||||
if self.path == '/api/v1/getparams.execute':
|
||||
args = self.args()
|
||||
self.reply({
|
||||
"output": {
|
||||
"parameters": [
|
||||
{
|
||||
"key1": "val1",
|
||||
"key2": "val2"
|
||||
},
|
||||
{
|
||||
"key1": "val2",
|
||||
"key2": "val2"
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
else:
|
||||
self.unsupported()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
httpd = HTTPServer(('', 4355), Plugin)
|
||||
httpd.serve_forever()
|
||||
```
|
||||
|
||||
Execute getparams with curl :
|
||||
|
||||
```
|
||||
curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer string-password" -d \
|
||||
'{
|
||||
"applicationSetName": "fake-appset",
|
||||
"input": {
|
||||
"parameters": {
|
||||
"param1": "value1"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Some things to note here:
|
||||
|
||||
- You only need to implement the calls `/api/v1/getparams.execute`
|
||||
- You should check that the `Authorization` header contains the same bearer value as `/var/run/argo/token`. Return 403 if not
|
||||
- The input parameters are included in the request body and can be accessed using the `input.parameters` variable.
|
||||
- The output must always be a list of object maps nested under the `output.parameters` key in a map.
|
||||
- `generator.input.parameters` and `values` are reserved keys. If present in the plugin output, these keys will be overwritten by the
|
||||
contents of the `input.parameters` and `values` keys in the ApplicationSet's plugin generator spec.
|
||||
|
||||
## With matrix and pull request example
|
||||
|
||||
In the following example, the plugin implementation is returning a set of image digests for the given branch. The returned list contains only one item correspondng to the latest builded image for the branch.
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
name: fb-matrix
|
||||
spec:
|
||||
goTemplate: true
|
||||
generators:
|
||||
- matrix:
|
||||
generators:
|
||||
- pullRequest:
|
||||
github: ...
|
||||
requeueAfterSeconds: 30
|
||||
- plugin:
|
||||
configMapRef:
|
||||
name: cm-plugin
|
||||
input:
|
||||
parameters:
|
||||
branch: "{{.branch}}" # provided by generator pull request
|
||||
values:
|
||||
branchLink: "https://git.example.com/org/repo/tree/{{.branch}}"
|
||||
template:
|
||||
metadata:
|
||||
name: "fb-matrix-{{.branch}}"
|
||||
spec:
|
||||
source:
|
||||
repoURL: "https://github.com/myorg/myrepo.git"
|
||||
targetRevision: "HEAD"
|
||||
path: charts/my-chart
|
||||
helm:
|
||||
releaseName: fb-matrix-{{.branch}}
|
||||
valueFiles:
|
||||
- values.yaml
|
||||
values: |
|
||||
front:
|
||||
image: myregistry:{{.branch}}@{{ .digestFront }} # digestFront is generated by the plugin
|
||||
back:
|
||||
image: myregistry:{{.branch}}@{{ .digestBack }} # digestBack is generated by the plugin
|
||||
project: default
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: "{{.branch}}"
|
||||
info:
|
||||
- name: Link to the Application's branch
|
||||
value: "{{values.branchLink}}"
|
||||
```
|
||||
|
||||
To illustrate :
|
||||
|
||||
- The generator pullRequest would return, for example, 2 branches: `feature-branch-1` and `feature-branch-2`.
|
||||
|
||||
- The generator plugin would then perform 2 requests as follows :
|
||||
|
||||
```shell
|
||||
curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer string-password" -d \
|
||||
'{
|
||||
"applicationSetName": "fb-matrix",
|
||||
"input": {
|
||||
"parameters": {
|
||||
"branch": "feature-branch-1"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
Then,
|
||||
|
||||
```shell
|
||||
curl http://localhost:4355/api/v1/getparams.execute -H "Authorization: Bearer string-password" -d \
|
||||
'{
|
||||
"applicationSetName": "fb-matrix",
|
||||
"input": {
|
||||
"parameters": {
|
||||
"branch": "feature-branch-2"
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
For each call, it would return a unique result such as :
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"parameters": [
|
||||
{
|
||||
"digestFront": "sha256:a3f18c17771cc1051b790b453a0217b585723b37f14b413ad7c5b12d4534d411",
|
||||
"digestBack": "sha256:4411417d614d5b1b479933b7420079671facd434fd42db196dc1f4cc55ba13ce"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then,
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"parameters": [
|
||||
{
|
||||
"digestFront": "sha256:7c20b927946805124f67a0cb8848a8fb1344d16b4d0425d63aaa3f2427c20497",
|
||||
"digestBack": "sha256:e55e7e40700bbab9e542aba56c593cb87d680cefdfba3dd2ab9cfcb27ec384c2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In this example, by combining the two, you ensure that one or more pull requests are available and that the generated tag has been properly generated. This wouldn't have been possible with just a commit hash because a hash alone does not certify the success of the build.
|
||||
|
|
@ -4,7 +4,7 @@ Generators are responsible for generating *parameters*, which are then rendered
|
|||
|
||||
Generators are primarily based on the data source that they use to generate the template parameters. For example: the List generator provides a set of parameters from a *literal list*, the Cluster generator uses the *Argo CD cluster list* as a source, the Git generator uses files/directories from a *Git repository*, and so.
|
||||
|
||||
As of this writing there are eight generators:
|
||||
As of this writing there are nine generators:
|
||||
|
||||
- [List generator](Generators-List.md): The List generator allows you to target Argo CD Applications to clusters based on a fixed list of cluster name/URL values.
|
||||
- [Cluster generator](Generators-Cluster.md): The Cluster generator allows you to target Argo CD Applications to clusters, based on the list of clusters defined within (and managed by) Argo CD (which includes automatically responding to cluster addition/removal events from Argo CD).
|
||||
|
|
@ -14,6 +14,7 @@ As of this writing there are eight generators:
|
|||
- [SCM Provider generator](Generators-SCM-Provider.md): The SCM Provider generator uses the API of an SCM provider (eg GitHub) to automatically discover repositories within an organization.
|
||||
- [Pull Request generator](Generators-Pull-Request.md): The Pull Request generator uses the API of an SCMaaS provider (eg GitHub) to automatically discover open pull requests within an repository.
|
||||
- [Cluster Decision Resource generator](Generators-Cluster-Decision-Resource.md): The Cluster Decision Resource generator is used to interface with Kubernetes custom resources that use custom resource-specific logic to decide which set of Argo CD clusters to deploy to.
|
||||
- [Plugin generator](Generators-Plugin.md): The Plugin generator make RPC HTTP request to provide parameters.
|
||||
|
||||
All generators can be filtered by using the [Post Selector](Generators-Post-Selector.md)
|
||||
|
||||
|
|
|
|||
216
docs/proposals/applicationset-plugin-generator.md
Normal file
216
docs/proposals/applicationset-plugin-generator.md
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
---
|
||||
title: applicationset-plugin-generator
|
||||
authors:
|
||||
- "@binboum"
|
||||
- "@scrocquesel"
|
||||
sponsors:
|
||||
- TBD
|
||||
reviewers:
|
||||
- TBD
|
||||
approvers:
|
||||
- "@alexmt"
|
||||
- TBD
|
||||
|
||||
creation-date: 2022-03-21
|
||||
last-updated: 2022-03-21
|
||||
---
|
||||
|
||||
# ApplicationSet `plugin` generator
|
||||
|
||||
Provide a generator that request its values through a RPC call.
|
||||
|
||||
## Summary
|
||||
|
||||
ApplicationSet generators are useful for modeling templates using external data sources to deploy applications.
|
||||
|
||||
Today, generators have been developed based on the needs of the community, and when a new need arises, it's necessary to modify the Appset codebase.
|
||||
|
||||
The proposal here is to have a "plugin" generator that would allow extending the codebase according to specific needs, without having to modify it directly.
|
||||
|
||||
## Motivation
|
||||
|
||||
Using the current generators, we sometimes encounter a need that arises, which may or may not be useful for the community. In such cases, several procedures need to be undertaken to make the modification, and sometimes it may be rejected because it's not in everyone's interest.
|
||||
|
||||
The plugin approach also reduces the burden on community developers by externalizing feature requests into plugins that are outside the Appset controller's scope. From a security and scalability perspective, this can be advantageous.
|
||||
|
||||
With this approach, it becomes possible to offer a catalog of plugins and encourage people with specific needs to develop standalone plugins that are independent of the controller's codebase.
|
||||
|
||||
### Goals
|
||||
|
||||
Empowering community developers to develop and use plugins that extend the list of generators can be a significant advantage. It would be possible to offer a page listing plugins maintained by the community, which can help promote the development of a rich ecosystem of plugins for various use cases. This can enhance the overall user experience by providing more options for generating application templates.
|
||||
|
||||
Additionally, allowing developers to create plugins and share them with the community can foster innovation and encourage experimentation with new features and functionalities. It can also reduce the workload on the Appset development team, enabling them to focus on core features and functionalities.
|
||||
|
||||
Overall, giving autonomy to community developers through plugins is a practical way to enhance the Appset platform and provide more value to users.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
The concept of the plugin should not undermine the spirit of GitOps by externalizing data outside of Git. The goal is to be complementary in specific contexts.
|
||||
|
||||
For example, when using one of the PullRequest generators, it's impossible to retrieve parameters related to the CI (only the commit hash is available), which limits the possibilities. By using a plugin, it's possible to retrieve the necessary parameters from a separate data source and use them to extend the functionality of the generator. This approach allows for greater flexibility and can help overcome limitations imposed by GitOps.
|
||||
|
||||
Overall, the use of plugins should be considered as a way to enhance the capabilities of existing tools and processes rather than as a replacement for them. By leveraging plugins, developers can take advantage of the strengths of different tools and technologies, resulting in a more robust and flexible development process.
|
||||
|
||||
## Proposal
|
||||
|
||||
### Add a new `generator` plugin
|
||||
|
||||
```
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: ApplicationSet
|
||||
metadata:
|
||||
name: fb-plugin
|
||||
namespace: argo-system
|
||||
spec:
|
||||
generators:
|
||||
- plugin:
|
||||
configMapRef: fb-plugin
|
||||
name: feature-branch-plugin
|
||||
params:
|
||||
repo: "my-repo"
|
||||
branch: "my-branch"
|
||||
requeueAfterSeconds: 10
|
||||
template:
|
||||
...
|
||||
```
|
||||
|
||||
### Add a configMap to configure the plugin
|
||||
|
||||
The configMap name must match the configMapRef value in the plugin configuration. The configMap must be in the namespace of argo.
|
||||
|
||||
```
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fb-plugin
|
||||
namespace: argo-system
|
||||
data:
|
||||
token: $plugin.myplugin.token # Alternatively $<some_K8S_secret>:plugin.myplugin.token
|
||||
baseUrl: http://myplugin.plugin.svc.cluster.local
|
||||
```
|
||||
|
||||
- token is used a a bearer token in the RPC request. It could be a [sensitive reference](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#sensitive-data-and-sso-client-secrets).
|
||||
|
||||
### Reconciliation logic
|
||||
|
||||
Here is a diagram describing what the plugin generator should do to get the params to return:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
alt generator is plugin
|
||||
Generator->>K8S: Get configmap {configMapRef}
|
||||
K8S-->>Generator: (url,token)
|
||||
Generator->>Plugin endpoint: POST {url}/v1/generator.getParams<br/>Authorization: Bearer {token}<br/>Content-Type: application/json<br/>{params}
|
||||
Plugin endpoint-->>Generator: []map{string}interface{}
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
### Use cases
|
||||
|
||||
#### Use case 1:
|
||||
As a user, I would like to enrich PullRequest generator params with digests of images generated by the pull request CI pipeline.
|
||||
|
||||
I could define a generator matrix like
|
||||
|
||||
```yaml
|
||||
generators:
|
||||
- matrix:
|
||||
generators:
|
||||
- pullRequest:
|
||||
github:
|
||||
owner: binboum
|
||||
repo: argo-test
|
||||
labels:
|
||||
- preview-matrix
|
||||
tokenRef:
|
||||
secretName: github-secret
|
||||
key: token
|
||||
- plugin:
|
||||
configMapRef: cm-plugin
|
||||
name: plugin-matrix
|
||||
params:
|
||||
repo: "argo-test"
|
||||
branch: "{{.branch}}"
|
||||
```
|
||||
|
||||
When pullRequest returns a new PR matching my labels, the plugin will be called with the branch name and would return a set of digests like
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"digestFront": "xxxxxxxx",
|
||||
"digestBack": "xxxxxxxx",
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Values can then be used in the template section :
|
||||
|
||||
```yaml
|
||||
template:
|
||||
metadata:
|
||||
name: "fb-matrix-{{.branch}}"
|
||||
spec:
|
||||
source:
|
||||
repoURL: "git@github.com:binboum/argo-test.git"
|
||||
targetRevision: "HEAD"
|
||||
path: charts/app-client
|
||||
helm:
|
||||
releaseName: feature-test-matrix-{{.branch}}
|
||||
valueFiles:
|
||||
- values.yaml
|
||||
values: |
|
||||
front:
|
||||
image: registry.my/argo-test/front:{{.branch}}@{{ .digestFront }}
|
||||
back:
|
||||
image: registry.my/argo-test/back:{{.branch}}@{{ .digestBack }}
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: "{{.branch}}"
|
||||
```
|
||||
|
||||
### Detailed examples
|
||||
|
||||
### Security Considerations
|
||||
|
||||
* Plugin server only has access to the params content. When deployed outside of the applicationset controller pod, operator must ensure the communication between applicationset controller and the plugin server is properly secured (https/network policy...). A few authentication mechanism are handled to help the plugin server authenticate the request.
|
||||
* For now, the response payload is considered trusted and returned params are used as-is upstream
|
||||
|
||||
### Risks and Mitigations
|
||||
|
||||
TBD
|
||||
|
||||
### Upgrade / Downgrade Strategy
|
||||
|
||||
On the evolution of the plugin, and calls :
|
||||
|
||||
The RPC method is standardized with a versioning system, which allows for a version parameter to be included in the API call. This makes it possible to avoid breaking changes in case of architecture changes in the future.
|
||||
|
||||
Thought that the contract interface with the plugin server is kept simple to reduce future changes and breaking changes
|
||||
|
||||
## Drawbacks
|
||||
|
||||
No idea
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. A design similar to Argo Workflow executor plugin :
|
||||
|
||||
```
|
||||
generators:
|
||||
- plugin:
|
||||
hello: {}
|
||||
```
|
||||
|
||||
A set of ConfigMaps or a specific CRDs to express configuration of the plugin endpoint would be walk by ApplicationSet server. For each configuration, call the plugin endpoint with the content of plugin until one return a valid response.
|
||||
|
||||
Reconciliation should be fast as fast as possible and trying out every endpoint to figure out which one is able to handle the plugin payload could induce a lot of delay.
|
||||
|
||||
Configuration rely on implicit and weakly typed convention which make the usage of the plugin less self documented.
|
||||
|
||||
2. Plugin server as defacto sidecars
|
||||
|
||||
Some magic could have inject a container image for the plugin in the ApplicationSet controller in a similar way, Argo Workflow does when creating a pod to execute a job.
|
||||
|
||||
Require an external controler or manual configuration. The plugin would not scale independently of the ApplicationSet controller.
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -101,6 +101,7 @@ nav:
|
|||
- operator-manual/applicationset/Generators-Cluster-Decision-Resource.md
|
||||
- operator-manual/applicationset/Generators-Pull-Request.md
|
||||
- operator-manual/applicationset/Generators-Post-Selector.md
|
||||
- operator-manual/applicationset/Generators-Plugin.md
|
||||
- Template fields:
|
||||
- operator-manual/applicationset/Template.md
|
||||
- operator-manual/applicationset/GoTemplate.md
|
||||
|
|
|
|||
|
|
@ -124,6 +124,8 @@ type ApplicationSetGenerator struct {
|
|||
|
||||
// Selector allows to post-filter all generator.
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,9,name=selector"`
|
||||
|
||||
Plugin *PluginGenerator `json:"plugin,omitempty" protobuf:"bytes,10,name=plugin"`
|
||||
}
|
||||
|
||||
// ApplicationSetNestedGenerator represents a generator nested within a combination-type generator (MatrixGenerator or
|
||||
|
|
@ -144,6 +146,8 @@ type ApplicationSetNestedGenerator struct {
|
|||
|
||||
// Selector allows to post-filter all generator.
|
||||
Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,9,name=selector"`
|
||||
|
||||
Plugin *PluginGenerator `json:"plugin,omitempty" protobuf:"bytes,10,name=plugin"`
|
||||
}
|
||||
|
||||
type ApplicationSetNestedGenerators []ApplicationSetNestedGenerator
|
||||
|
|
@ -159,6 +163,7 @@ type ApplicationSetTerminalGenerator struct {
|
|||
SCMProvider *SCMProviderGenerator `json:"scmProvider,omitempty" protobuf:"bytes,4,name=scmProvider"`
|
||||
ClusterDecisionResource *DuckTypeGenerator `json:"clusterDecisionResource,omitempty" protobuf:"bytes,5,name=clusterDecisionResource"`
|
||||
PullRequest *PullRequestGenerator `json:"pullRequest,omitempty" protobuf:"bytes,6,name=pullRequest"`
|
||||
Plugin *PluginGenerator `json:"plugin,omitempty" protobuf:"bytes,7,name=pullRequest"`
|
||||
}
|
||||
|
||||
type ApplicationSetTerminalGenerators []ApplicationSetTerminalGenerator
|
||||
|
|
@ -176,6 +181,7 @@ func (g ApplicationSetTerminalGenerators) toApplicationSetNestedGenerators() []A
|
|||
SCMProvider: terminalGenerator.SCMProvider,
|
||||
ClusterDecisionResource: terminalGenerator.ClusterDecisionResource,
|
||||
PullRequest: terminalGenerator.PullRequest,
|
||||
Plugin: terminalGenerator.Plugin,
|
||||
}
|
||||
}
|
||||
return nestedGenerators
|
||||
|
|
@ -559,6 +565,32 @@ type PullRequestGeneratorFilter struct {
|
|||
TargetBranchMatch *string `json:"targetBranchMatch,omitempty" protobuf:"bytes,2,opt,name=targetBranchMatch"`
|
||||
}
|
||||
|
||||
type PluginConfigMapRef struct {
|
||||
// Name of the ConfigMap
|
||||
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
|
||||
}
|
||||
|
||||
type PluginParameters map[string]apiextensionsv1.JSON
|
||||
|
||||
type PluginInput struct {
|
||||
// Parameters contains the information to pass to the plugin. It is a map. The keys must be strings, and the
|
||||
// values can be any type.
|
||||
Parameters PluginParameters `json:"parameters,omitempty" protobuf:"bytes,1,name=parameters"`
|
||||
}
|
||||
|
||||
// PluginGenerator defines connection info specific to Plugin.
|
||||
type PluginGenerator struct {
|
||||
ConfigMapRef PluginConfigMapRef `json:"configMapRef" protobuf:"bytes,1,name=configMapRef"`
|
||||
Input PluginInput `json:"input,omitempty" protobuf:"bytes,2,name=input"`
|
||||
// RequeueAfterSeconds determines how long the ApplicationSet controller will wait before reconciling the ApplicationSet again.
|
||||
RequeueAfterSeconds *int64 `json:"requeueAfterSeconds,omitempty" protobuf:"varint,3,opt,name=requeueAfterSeconds"`
|
||||
Template ApplicationSetTemplate `json:"template,omitempty" protobuf:"bytes,4,name=template"`
|
||||
|
||||
// Values contains key/value pairs which are passed directly as parameters to the template. These values will not be
|
||||
// sent as parameters to the plugin.
|
||||
Values map[string]string `json:"values,omitempty" protobuf:"bytes,5,name=values"`
|
||||
}
|
||||
|
||||
// ApplicationSetStatus defines the observed state of ApplicationSet
|
||||
type ApplicationSetStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -231,6 +231,8 @@ message ApplicationSetGenerator {
|
|||
|
||||
// Selector allows to post-filter all generator.
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.LabelSelector selector = 9;
|
||||
|
||||
optional PluginGenerator plugin = 10;
|
||||
}
|
||||
|
||||
// ApplicationSetList contains a list of ApplicationSet
|
||||
|
|
@ -265,6 +267,8 @@ message ApplicationSetNestedGenerator {
|
|||
|
||||
// Selector allows to post-filter all generator.
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.LabelSelector selector = 9;
|
||||
|
||||
optional PluginGenerator plugin = 10;
|
||||
}
|
||||
|
||||
message ApplicationSetRolloutStep {
|
||||
|
|
@ -354,6 +358,8 @@ message ApplicationSetTerminalGenerator {
|
|||
optional DuckTypeGenerator clusterDecisionResource = 5;
|
||||
|
||||
optional PullRequestGenerator pullRequest = 6;
|
||||
|
||||
optional PluginGenerator plugin = 7;
|
||||
}
|
||||
|
||||
// ApplicationSource contains all required information about the source of an application
|
||||
|
|
@ -1196,6 +1202,33 @@ message OverrideIgnoreDiff {
|
|||
repeated string managedFieldsManagers = 3;
|
||||
}
|
||||
|
||||
message PluginConfigMapRef {
|
||||
// Name of the ConfigMap
|
||||
optional string name = 1;
|
||||
}
|
||||
|
||||
// PluginGenerator defines connection info specific to Plugin.
|
||||
message PluginGenerator {
|
||||
optional PluginConfigMapRef configMapRef = 1;
|
||||
|
||||
optional PluginInput input = 2;
|
||||
|
||||
// RequeueAfterSeconds determines how long the ApplicationSet controller will wait before reconciling the ApplicationSet again.
|
||||
optional int64 requeueAfterSeconds = 3;
|
||||
|
||||
optional ApplicationSetTemplate template = 4;
|
||||
|
||||
// Values contains key/value pairs which are passed directly as parameters to the template. These values will not be
|
||||
// sent as parameters to the plugin.
|
||||
map<string, string> values = 5;
|
||||
}
|
||||
|
||||
message PluginInput {
|
||||
// Parameters contains the information to pass to the plugin. It is a map. The keys must be strings, and the
|
||||
// values can be any type.
|
||||
map<string, k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1.JSON> parameters = 1;
|
||||
}
|
||||
|
||||
// ProjectRole represents a role that has access to a project
|
||||
message ProjectRole {
|
||||
// Name is a name for this role
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
|||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OrphanedResourceKey": schema_pkg_apis_application_v1alpha1_OrphanedResourceKey(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OrphanedResourcesMonitorSettings": schema_pkg_apis_application_v1alpha1_OrphanedResourcesMonitorSettings(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OverrideIgnoreDiff": schema_pkg_apis_application_v1alpha1_OverrideIgnoreDiff(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginConfigMapRef": schema_pkg_apis_application_v1alpha1_PluginConfigMapRef(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator": schema_pkg_apis_application_v1alpha1_PluginGenerator(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginInput": schema_pkg_apis_application_v1alpha1_PluginInput(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ProjectRole": schema_pkg_apis_application_v1alpha1_ProjectRole(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator": schema_pkg_apis_application_v1alpha1_PullRequestGenerator(ref),
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGeneratorBitbucketServer": schema_pkg_apis_application_v1alpha1_PullRequestGeneratorBitbucketServer(ref),
|
||||
|
|
@ -936,11 +939,16 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetGenerator(ref common.Ref
|
|||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"),
|
||||
},
|
||||
},
|
||||
"plugin": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.MatrixGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.MergeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.MatrixGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.MergeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1048,11 +1056,16 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetNestedGenerator(ref comm
|
|||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"),
|
||||
},
|
||||
},
|
||||
"plugin": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", "k8s.io/apimachinery/pkg/apis/meta/v1.LabelSelector"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1407,11 +1420,16 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetTerminalGenerator(ref co
|
|||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator"),
|
||||
},
|
||||
},
|
||||
"plugin": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator"},
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ClusterGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.DuckTypeGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.GitGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ListGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PullRequestGenerator", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.SCMProviderGenerator"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4279,6 +4297,113 @@ func schema_pkg_apis_application_v1alpha1_OverrideIgnoreDiff(ref common.Referenc
|
|||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_PluginConfigMapRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Name of the ConfigMap",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_PluginGenerator(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "PluginGenerator defines connection info specific to Plugin.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"configMapRef": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginConfigMapRef"),
|
||||
},
|
||||
},
|
||||
"input": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginInput"),
|
||||
},
|
||||
},
|
||||
"requeueAfterSeconds": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "RequeueAfterSeconds determines how long the ApplicationSet controller will wait before reconciling the ApplicationSet again.",
|
||||
Type: []string{"integer"},
|
||||
Format: "int64",
|
||||
},
|
||||
},
|
||||
"template": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSetTemplate"),
|
||||
},
|
||||
},
|
||||
"values": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Values contains key/value pairs which are passed directly as parameters to the template. These values will not be sent as parameters to the plugin.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"configMapRef"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSetTemplate", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginConfigMapRef", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.PluginInput"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_PluginInput(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"parameters": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Parameters contains the information to pass to the plugin. It is a map. The keys must be strings, and the values can be any type.",
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_application_v1alpha1_ProjectRole(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
|
|
|||
|
|
@ -452,6 +452,11 @@ func (in *ApplicationSetGenerator) DeepCopyInto(out *ApplicationSetGenerator) {
|
|||
*out = new(v1.LabelSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Plugin != nil {
|
||||
in, out := &in.Plugin, &out.Plugin
|
||||
*out = new(PluginGenerator)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -546,6 +551,11 @@ func (in *ApplicationSetNestedGenerator) DeepCopyInto(out *ApplicationSetNestedG
|
|||
*out = new(v1.LabelSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Plugin != nil {
|
||||
in, out := &in.Plugin, &out.Plugin
|
||||
*out = new(PluginGenerator)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -829,6 +839,11 @@ func (in *ApplicationSetTerminalGenerator) DeepCopyInto(out *ApplicationSetTermi
|
|||
*out = new(PullRequestGenerator)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Plugin != nil {
|
||||
in, out := &in.Plugin, &out.Plugin
|
||||
*out = new(PluginGenerator)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -2497,6 +2512,98 @@ func (in *OverrideIgnoreDiff) DeepCopy() *OverrideIgnoreDiff {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PluginConfigMapRef) DeepCopyInto(out *PluginConfigMapRef) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConfigMapRef.
|
||||
func (in *PluginConfigMapRef) DeepCopy() *PluginConfigMapRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PluginConfigMapRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PluginGenerator) DeepCopyInto(out *PluginGenerator) {
|
||||
*out = *in
|
||||
out.ConfigMapRef = in.ConfigMapRef
|
||||
in.Input.DeepCopyInto(&out.Input)
|
||||
if in.RequeueAfterSeconds != nil {
|
||||
in, out := &in.RequeueAfterSeconds, &out.RequeueAfterSeconds
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
in.Template.DeepCopyInto(&out.Template)
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginGenerator.
|
||||
func (in *PluginGenerator) DeepCopy() *PluginGenerator {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PluginGenerator)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PluginInput) DeepCopyInto(out *PluginInput) {
|
||||
*out = *in
|
||||
if in.Parameters != nil {
|
||||
in, out := &in.Parameters, &out.Parameters
|
||||
*out = make(PluginParameters, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginInput.
|
||||
func (in *PluginInput) DeepCopy() *PluginInput {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PluginInput)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in PluginParameters) DeepCopyInto(out *PluginParameters) {
|
||||
{
|
||||
in := &in
|
||||
*out = make(PluginParameters, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginParameters.
|
||||
func (in PluginParameters) DeepCopy() PluginParameters {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PluginParameters)
|
||||
in.DeepCopyInto(out)
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ProjectRole) DeepCopyInto(out *ProjectRole) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -2001,7 +2001,7 @@ func ReplaceMapSecrets(obj map[string]interface{}, secretValues map[string]strin
|
|||
case []interface{}:
|
||||
newObj[k] = replaceListSecrets(val, secretValues)
|
||||
case string:
|
||||
newObj[k] = replaceStringSecret(val, secretValues)
|
||||
newObj[k] = ReplaceStringSecret(val, secretValues)
|
||||
default:
|
||||
newObj[k] = val
|
||||
}
|
||||
|
|
@ -2018,7 +2018,7 @@ func replaceListSecrets(obj []interface{}, secretValues map[string]string) []int
|
|||
case []interface{}:
|
||||
newObj[i] = replaceListSecrets(val, secretValues)
|
||||
case string:
|
||||
newObj[i] = replaceStringSecret(val, secretValues)
|
||||
newObj[i] = ReplaceStringSecret(val, secretValues)
|
||||
default:
|
||||
newObj[i] = val
|
||||
}
|
||||
|
|
@ -2026,8 +2026,8 @@ func replaceListSecrets(obj []interface{}, secretValues map[string]string) []int
|
|||
return newObj
|
||||
}
|
||||
|
||||
// replaceStringSecret checks if given string is a secret key reference ( starts with $ ) and returns corresponding value from provided map
|
||||
func replaceStringSecret(val string, secretValues map[string]string) string {
|
||||
// ReplaceStringSecret checks if given string is a secret key reference ( starts with $ ) and returns corresponding value from provided map
|
||||
func ReplaceStringSecret(val string, secretValues map[string]string) string {
|
||||
if val == "" || !strings.HasPrefix(val, "$") {
|
||||
return val
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1446,3 +1446,18 @@ allowedAudiences: ["aud1", "aud2"]`},
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceStringSecret(t *testing.T) {
|
||||
secretValues := map[string]string{"my-secret-key": "my-secret-value"}
|
||||
result := ReplaceStringSecret("$my-secret-key", secretValues)
|
||||
assert.Equal(t, "my-secret-value", result)
|
||||
|
||||
result = ReplaceStringSecret("$invalid-secret-key", secretValues)
|
||||
assert.Equal(t, "$invalid-secret-key", result)
|
||||
|
||||
result = ReplaceStringSecret("", secretValues)
|
||||
assert.Equal(t, "", result)
|
||||
|
||||
result = ReplaceStringSecret("my-value", secretValues)
|
||||
assert.Equal(t, "my-value", result)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue