diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index 952fae0e60d01..022b9f8bca738 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -430,8 +430,16 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argov var firstError error var applicationSetReason argov1alpha1.ApplicationSetReasonType + if (applicationSetInfo.Spec.Template == nil && applicationSetInfo.Spec.StringTemplate == nil) || + (applicationSetInfo.Spec.Template != nil && applicationSetInfo.Spec.StringTemplate != nil) { + firstError = fmt.Errorf("application set spec should have either template or stringTemplate defined") + applicationSetReason = argov1alpha1.ApplicationSetReasonErrorOccurred + + return res, applicationSetReason, firstError + } + for _, requestedGenerator := range applicationSetInfo.Spec.Generators { - t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]interface{}{}) + t, err := generators.Transform(requestedGenerator, r.Generators, *applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]interface{}{}) if err != nil { log.WithError(err).WithField("generator", requestedGenerator). Error("error generating application from params") @@ -446,7 +454,7 @@ func (r *ApplicationSetReconciler) generateApplications(applicationSetInfo argov tmplApplication := getTempApplication(a.Template) for _, p := range a.Params { - app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate) + app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.StringTemplate, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate) if err != nil { log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). Error("error generating application from params") diff --git a/applicationset/controllers/applicationset_controller_test.go b/applicationset/controllers/applicationset_controller_test.go index 4a45da8f42efc..bc642922bce98 100644 --- a/applicationset/controllers/applicationset_controller_test.go +++ b/applicationset/controllers/applicationset_controller_test.go @@ -60,7 +60,8 @@ func (g *generatorMock) GetRequeueAfter(appSetGenerator *argov1alpha1.Applicatio return args.Get(0).(time.Duration) } -func (r *rendererMock) RenderTemplateParams(tmpl *argov1alpha1.Application, syncPolicy *argov1alpha1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argov1alpha1.Application, error) { +func (r *rendererMock) RenderTemplateParams(tmpl *argov1alpha1.Application, stringTemplate *argov1alpha1.ApplicationSetStringTemplate, syncPolicy *argov1alpha1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argov1alpha1.Application, error) { + args := r.Called(tmpl, params, useGoTemplate) if args.Error(1) != nil { @@ -188,7 +189,7 @@ func TestExtractApplications(t *testing.T) { }, Spec: argov1alpha1.ApplicationSetSpec{ Generators: []argov1alpha1.ApplicationSetGenerator{generator}, - Template: cc.template, + Template: &cc.template, }, }) @@ -301,7 +302,7 @@ func TestMergeTemplateApplications(t *testing.T) { }, Spec: argov1alpha1.ApplicationSetSpec{ Generators: []argov1alpha1.ApplicationSetGenerator{generator}, - Template: cc.template, + Template: &cc.template, }, }, ) @@ -371,7 +372,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -429,7 +430,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -487,7 +488,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -549,7 +550,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -609,7 +610,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -681,7 +682,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", Source: argov1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"}, @@ -761,7 +762,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -903,7 +904,7 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -1065,7 +1066,7 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -1193,7 +1194,7 @@ func TestCreateApplications(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -1250,7 +1251,7 @@ func TestCreateApplications(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -1362,7 +1363,7 @@ func TestDeleteInCluster(t *testing.T) { Namespace: "namespace", }, Spec: argov1alpha1.ApplicationSetSpec{ - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ Spec: argov1alpha1.ApplicationSpec{ Project: "project", }, @@ -1805,7 +1806,7 @@ func TestReconcilerValidationErrorBehaviour(t *testing.T) { }, }, }, - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: argov1alpha1.ApplicationSetTemplateMeta{ Name: "{{.cluster}}", Namespace: "argocd", @@ -1890,7 +1891,7 @@ func TestSetApplicationSetStatusCondition(t *testing.T) { }}, }}, }, - Template: argov1alpha1.ApplicationSetTemplate{}, + Template: &argov1alpha1.ApplicationSetTemplate{}, }, } @@ -2017,7 +2018,7 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { Generators: []argov1alpha1.ApplicationSetGenerator{{ PullRequest: &argov1alpha1.PullRequestGenerator{}, }}, - Template: cases.template, + Template: &cases.template, }, }, ) @@ -2104,7 +2105,7 @@ func TestPolicies(t *testing.T) { }, }, }, - Template: argov1alpha1.ApplicationSetTemplate{ + Template: &argov1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: argov1alpha1.ApplicationSetTemplateMeta{ Name: "{{.name}}", Namespace: "argocd", diff --git a/applicationset/generators/generator_spec_processor.go b/applicationset/generators/generator_spec_processor.go index 4e08816e3e0c0..c910bed837984 100644 --- a/applicationset/generators/generator_spec_processor.go +++ b/applicationset/generators/generator_spec_processor.go @@ -1,8 +1,8 @@ package generators import ( - "fmt" "encoding/json" + "fmt" "reflect" "github.com/argoproj/argo-cd/v2/applicationset/utils" @@ -25,7 +25,7 @@ type TransformResult struct { Template argoprojiov1alpha1.ApplicationSetTemplate } -//Transform a spec generator to list of paramSets and a template +// Transform a spec generator to list of paramSets and a template func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, allGenerators map[string]Generator, baseTemplate argoprojiov1alpha1.ApplicationSetTemplate, appSet *argoprojiov1alpha1.ApplicationSet, genParams map[string]interface{}) ([]TransformResult, error) { selector, err := metav1.LabelSelectorAsSelector(requestedGenerator.Selector) if err != nil { @@ -39,7 +39,7 @@ func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, al generators := GetRelevantGenerators(&requestedGenerator, allGenerators) for _, g := range generators { // we call mergeGeneratorTemplate first because GenerateParams might be more costly so we want to fail fast if there is an error - mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, baseTemplate) + mergedTemplate, err := mergeGeneratorTemplate(g, &requestedGenerator, &baseTemplate) if err != nil { log.WithError(err).WithField("generator", g). Error("error generating params") @@ -122,12 +122,18 @@ func GetRelevantGenerators(requestedGenerator *argoprojiov1alpha1.ApplicationSet return res } -func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) { +func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetTemplate *argoprojiov1alpha1.ApplicationSetTemplate) (argoprojiov1alpha1.ApplicationSetTemplate, error) { // Make a copy of the value from `GetTemplate()` before merge, rather than copying directly into // the provided parameter (which will touch the original resource object returned by client-go) dest := g.GetTemplate(requestedGenerator).DeepCopy() - err := mergo.Merge(dest, applicationSetTemplate) + var err error + + if applicationSetTemplate != nil { + err = mergo.Merge(dest, applicationSetTemplate) + } else { + log.Warn("generator template won't be applied when standard application template is not used") + } return *dest, err } diff --git a/applicationset/utils/utils.go b/applicationset/utils/utils.go index 254478ee45c7f..b75df6a63b290 100644 --- a/applicationset/utils/utils.go +++ b/applicationset/utils/utils.go @@ -14,6 +14,7 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/valyala/fasttemplate" + "sigs.k8s.io/yaml" log "github.com/sirupsen/logrus" @@ -27,11 +28,19 @@ func init() { delete(sprigFuncMap, "env") delete(sprigFuncMap, "expandenv") delete(sprigFuncMap, "getHostByName") - sprigFuncMap["normalize"] = SanitizeName + extraFuncMap := template.FuncMap{ + "toYaml": ToYaml, + "normalize": SanitizeName, + } + + for name, f := range extraFuncMap { + sprigFuncMap[name] = f + } + } type Renderer interface { - RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argoappsv1.Application, error) + RenderTemplateParams(tmpl *argoappsv1.Application, stringTemplate *argoappsv1.ApplicationSetStringTemplate, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argoappsv1.Application, error) } type Render struct { @@ -172,7 +181,8 @@ func (r *Render) deeplyReplace(copy, original reflect.Value, replaceMap map[stri return nil } -func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argoappsv1.Application, error) { +func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, stringTemplate *argoappsv1.ApplicationSetStringTemplate, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool) (*argoappsv1.Application, error) { + if tmpl == nil { return nil, fmt.Errorf("application template is empty ") } @@ -180,16 +190,28 @@ func (r *Render) RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy * if len(params) == 0 { return tmpl, nil } + var replacedTmpl *argoappsv1.Application + if stringTemplate == nil { + original := reflect.ValueOf(tmpl) + copy := reflect.New(original.Type()).Elem() - original := reflect.ValueOf(tmpl) - copy := reflect.New(original.Type()).Elem() + if err := r.deeplyReplace(copy, original, params, useGoTemplate); err != nil { + return nil, err + } - if err := r.deeplyReplace(copy, original, params, useGoTemplate); err != nil { - return nil, err + replacedTmpl = copy.Interface().(*argoappsv1.Application) + } else { + replacedTmplStr, err := r.Replace(string(*stringTemplate), params, true) + if err != nil { + return nil, err + } + // UnmarshalStrict to fail early and raise the fact that template + // result produced not what is expected + err = yaml.UnmarshalStrict([]byte(replacedTmplStr), &replacedTmpl) + if err != nil { + return nil, err + } } - - replacedTmpl := copy.Interface().(*argoappsv1.Application) - // Add the 'resources-finalizer' finalizer if: // The template application doesn't have any finalizers, and: // a) there is no syncPolicy, or @@ -353,3 +375,11 @@ func SanitizeName(name string) string { return strings.Trim(name, "-.") } + +func ToYaml(v interface{}) (string, error) { + data, err := yaml.Marshal(v) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/applicationset/utils/utils_test.go b/applicationset/utils/utils_test.go index 38c6aee2bf1f1..b2a8fd04a715a 100644 --- a/applicationset/utils/utils_test.go +++ b/applicationset/utils/utils_test.go @@ -16,6 +16,105 @@ import ( argoappsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" ) +func TestRenderTemplateParamsStringTemplate(t *testing.T) { + + // Believe it or not, this is actually less complex than the equivalent solution using reflection + fieldMap := map[string]func(app *argoappsetv1.Application) *string{} + fieldMap["Path"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Source.Path } + fieldMap["RepoURL"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Source.RepoURL } + fieldMap["TargetRevision"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Source.TargetRevision } + fieldMap["Chart"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Source.Chart } + + fieldMap["Server"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Destination.Server } + fieldMap["Namespace"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Destination.Namespace } + fieldMap["Name"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Destination.Name } + + fieldMap["Project"] = func(app *argoappsetv1.Application) *string { return &app.Spec.Project } + + emptyApplication := &argoappsetv1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{"annotation-key": "annotation-value", "annotation-key2": "annotation-value2"}, + Labels: map[string]string{"label-key": "label-value", "label-key2": "label-value2"}, + CreationTimestamp: metav1.NewTime(time.Now()), + UID: types.UID("d546da12-06b7-4f9a-8ea2-3adb16a20e2b"), + Name: "application-one", + Namespace: "default", + }, + Spec: argoappsetv1.ApplicationSpec{ + Source: argoappsetv1.ApplicationSource{ + Path: "", + RepoURL: "", + TargetRevision: "", + Chart: "", + }, + Destination: argoappsetv1.ApplicationDestination{ + Server: "", + Namespace: "", + Name: "", + }, + Project: "", + }, + } + + tests := []struct { + name string + stringTemplate argoappsetv1.ApplicationSetStringTemplate + params map[string]interface{} + expectedVal string + errorMessage string + }{ + { + name: "text template", + stringTemplate: argoappsetv1.ApplicationSetStringTemplate(`metadata: + name: {{ .name }} +spec: + project: default + source: + repoURL: test-repo + targetRevision: HEAD + path: test-path + destination: + server: 'test-server' + namespace: guestbook + syncPolicy: + automated: + prune: {{ .prune }} + selfHeal: {{ .selfHeal }} +`), + expectedVal: "[volume-one][volume-two]", + params: map[string]interface{}{ + "name": "test-name", + "prune": false, + "selfHeal": true, + }, + }, + } + + for _, test := range tests { + + t.Run(test.name, func(t *testing.T) { + + // Clone the template application + application := emptyApplication.DeepCopy() + + // Render the cloned application, into a new application + render := Render{} + newApplication, err := render.RenderTemplateParams(application, &test.stringTemplate, nil, test.params, true) + + // Retrieve the value of the target field from the newApplication, then verify that + // the target field has been templated into the expected value + if test.errorMessage != "" { + assert.Error(t, err) + assert.Equal(t, test.errorMessage, err.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, newApplication.ObjectMeta.Name, "test-name") + } + }) + } + +} + func TestRenderTemplateParams(t *testing.T) { // Believe it or not, this is actually less complex than the equivalent solution using reflection @@ -174,7 +273,7 @@ func TestRenderTemplateParams(t *testing.T) { // Render the cloned application, into a new application render := Render{} - newApplication, err := render.RenderTemplateParams(application, nil, test.params, false) + newApplication, err := render.RenderTemplateParams(application, nil, nil, test.params, false) // Retrieve the value of the target field from the newApplication, then verify that // the target field has been templated into the expected value @@ -439,7 +538,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) { // Render the cloned application, into a new application render := Render{} - newApplication, err := render.RenderTemplateParams(application, nil, test.params, true) + newApplication, err := render.RenderTemplateParams(application, nil, nil, test.params, true) // Retrieve the value of the target field from the newApplication, then verify that // the target field has been templated into the expected value @@ -480,7 +579,7 @@ func TestRenderTemplateKeys(t *testing.T) { } render := Render{} - newApplication, err := render.RenderTemplateParams(application, nil, params, false) + newApplication, err := render.RenderTemplateParams(application, nil, nil, params, false) require.NoError(t, err) require.Contains(t, newApplication.ObjectMeta.Annotations, "annotation-some-key") assert.Equal(t, newApplication.ObjectMeta.Annotations["annotation-some-key"], "annotation-some-value") @@ -500,7 +599,7 @@ func TestRenderTemplateKeys(t *testing.T) { } render := Render{} - newApplication, err := render.RenderTemplateParams(application, nil, params, true) + newApplication, err := render.RenderTemplateParams(application, nil, nil, params, true) require.NoError(t, err) require.Contains(t, newApplication.ObjectMeta.Annotations, "annotation-some-key") assert.Equal(t, newApplication.ObjectMeta.Annotations["annotation-some-key"], "annotation-some-value") @@ -601,7 +700,7 @@ func TestRenderTemplateParamsFinalizers(t *testing.T) { // Render the cloned application, into a new application render := Render{} - res, err := render.RenderTemplateParams(application, c.syncPolicy, params, true) + res, err := render.RenderTemplateParams(application, nil, c.syncPolicy, params, true) assert.Nil(t, err) assert.ElementsMatch(t, res.Finalizers, c.expectedFinalizers) diff --git a/cmd/argocd/commands/applicationset_test.go b/cmd/argocd/commands/applicationset_test.go index 9937b183e5c29..1e7e51e8a3b61 100644 --- a/cmd/argocd/commands/applicationset_test.go +++ b/cmd/argocd/commands/applicationset_test.go @@ -45,7 +45,7 @@ func TestPrintApplicationSetTable(t *testing.T) { }, }, }, - Template: arogappsetv1.ApplicationSetTemplate{ + Template: &arogappsetv1.ApplicationSetTemplate{ Spec: v1alpha1.ApplicationSpec{ Project: "default", }, diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index 1f9a35cf75579..4f61b1fb302f4 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -9344,6 +9344,8 @@ spec: type: array goTemplate: type: boolean + stringTemplate: + type: string syncPolicy: properties: preserveResourcesOnDeletion: diff --git a/manifests/crds/applicationset-crd.yaml b/manifests/crds/applicationset-crd.yaml index fa795e6095cce..0192c72716d32 100644 --- a/manifests/crds/applicationset-crd.yaml +++ b/manifests/crds/applicationset-crd.yaml @@ -7031,6 +7031,8 @@ spec: type: array goTemplate: type: boolean + stringTemplate: + type: string syncPolicy: properties: preserveResourcesOnDeletion: diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index 9526b4a3c735c..a308b2d606a93 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -9344,6 +9344,8 @@ spec: type: array goTemplate: type: boolean + stringTemplate: + type: string syncPolicy: properties: preserveResourcesOnDeletion: diff --git a/manifests/install.yaml b/manifests/install.yaml index 192ad144a8ffd..9df3460378f4f 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -9344,6 +9344,8 @@ spec: type: array goTemplate: type: boolean + stringTemplate: + type: string syncPolicy: properties: preserveResourcesOnDeletion: diff --git a/pkg/apis/application/v1alpha1/applicationset_types.go b/pkg/apis/application/v1alpha1/applicationset_types.go index 29dbd8045ecdc..09f0a866ffd16 100644 --- a/pkg/apis/application/v1alpha1/applicationset_types.go +++ b/pkg/apis/application/v1alpha1/applicationset_types.go @@ -48,10 +48,11 @@ type ApplicationSet struct { // ApplicationSetSpec represents a class of application set state. type ApplicationSetSpec struct { - GoTemplate bool `json:"goTemplate,omitempty" protobuf:"bytes,1,name=goTemplate"` - Generators []ApplicationSetGenerator `json:"generators" protobuf:"bytes,2,name=generators"` - Template ApplicationSetTemplate `json:"template" protobuf:"bytes,3,name=template"` - SyncPolicy *ApplicationSetSyncPolicy `json:"syncPolicy,omitempty" protobuf:"bytes,4,name=syncPolicy"` + GoTemplate bool `json:"goTemplate,omitempty" protobuf:"bytes,1,name=goTemplate"` + Generators []ApplicationSetGenerator `json:"generators" protobuf:"bytes,2,name=generators"` + Template *ApplicationSetTemplate `json:"template" protobuf:"bytes,3,name=template"` + SyncPolicy *ApplicationSetSyncPolicy `json:"syncPolicy,omitempty" protobuf:"bytes,4,name=syncPolicy"` + StringTemplate *ApplicationSetStringTemplate `json:"stringTemplate,omitempty" protobuf:"bytes,5,opt,name=stringTemplate,casttype=ApplicationSetStringTemplate"` } // ApplicationSetSyncPolicy configures how generated Applications will relate to their @@ -67,6 +68,9 @@ type ApplicationSetTemplate struct { Spec ApplicationSpec `json:"spec" protobuf:"bytes,2,name=spec"` } +// ApplicationSetStringTemplate represents argocd ApplicationSpec without type check +type ApplicationSetStringTemplate string + // ApplicationSetTemplateMeta represents the Argo CD application fields that may // be used for Applications generated from the ApplicationSet (based on metav1.ObjectMeta) type ApplicationSetTemplateMeta struct { @@ -537,7 +541,7 @@ const ( // prefix "Info" means informational condition type ApplicationSetConditionType string -//ErrorOccurred / ParametersGenerated / TemplateRendered / ResourcesUpToDate +// ErrorOccurred / ParametersGenerated / TemplateRendered / ResourcesUpToDate const ( ApplicationSetConditionErrorOccurred ApplicationSetConditionType = "ErrorOccurred" ApplicationSetConditionParametersGenerated ApplicationSetConditionType = "ParametersGenerated" diff --git a/pkg/apis/application/v1alpha1/generated.proto b/pkg/apis/application/v1alpha1/generated.proto index 60f6440c03d7f..2fb5c71737f9c 100644 --- a/pkg/apis/application/v1alpha1/generated.proto +++ b/pkg/apis/application/v1alpha1/generated.proto @@ -245,6 +245,8 @@ message ApplicationSetSpec { optional ApplicationSetTemplate template = 3; optional ApplicationSetSyncPolicy syncPolicy = 4; + + optional string stringTemplate = 5; } // ApplicationSetStatus defines the observed state of ApplicationSet diff --git a/pkg/apis/application/v1alpha1/openapi_generated.go b/pkg/apis/application/v1alpha1/openapi_generated.go index b043631cae718..73c970894d97c 100644 --- a/pkg/apis/application/v1alpha1/openapi_generated.go +++ b/pkg/apis/application/v1alpha1/openapi_generated.go @@ -953,8 +953,7 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetSpec(ref common.Referenc }, "template": { SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSetTemplate"), + Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSetTemplate"), }, }, "syncPolicy": { @@ -962,6 +961,12 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetSpec(ref common.Referenc Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSetSyncPolicy"), }, }, + "stringTemplate": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"generators", "template"}, }, diff --git a/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go index e531a4715b0f2..efcf39e268294 100644 --- a/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go @@ -528,12 +528,21 @@ func (in *ApplicationSetSpec) DeepCopyInto(out *ApplicationSetSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - in.Template.DeepCopyInto(&out.Template) + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(ApplicationSetTemplate) + (*in).DeepCopyInto(*out) + } if in.SyncPolicy != nil { in, out := &in.SyncPolicy, &out.SyncPolicy *out = new(ApplicationSetSyncPolicy) **out = **in } + if in.StringTemplate != nil { + in, out := &in.StringTemplate, &out.StringTemplate + *out = new(ApplicationSetStringTemplate) + **out = **in + } return } diff --git a/test/e2e/applicationset_test.go b/test/e2e/applicationset_test.go index a5afead1bb24f..749445ac00212 100644 --- a/test/e2e/applicationset_test.go +++ b/test/e2e/applicationset_test.go @@ -77,7 +77,7 @@ func TestSimpleListGenerator(t *testing.T) { Name: "simple-list-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -170,7 +170,7 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -271,7 +271,7 @@ func TestSimpleGitDirectoryGenerator(t *testing.T) { Name: "simple-git-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -381,7 +381,7 @@ func TestSimpleGitDirectoryGeneratorGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -490,7 +490,7 @@ func TestSimpleGitFilesGenerator(t *testing.T) { Name: "simple-git-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -600,7 +600,7 @@ func TestSimpleGitFilesGeneratorGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster.name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -676,7 +676,7 @@ func TestSimpleGitFilesPreserveResourcesOnDeletion(t *testing.T) { Name: "simple-git-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{cluster.name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -737,7 +737,7 @@ func TestSimpleGitFilesPreserveResourcesOnDeletionGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster.name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -1015,7 +1015,7 @@ func TestSimpleSCMProviderGenerator(t *testing.T) { Name: "simple-scm-provider-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{ repository }}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -1089,7 +1089,7 @@ func TestSimpleSCMProviderGeneratorGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{ .repository }}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -1154,7 +1154,7 @@ func TestCustomApplicationFinalizers(t *testing.T) { Name: "simple-list-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ Name: "{{cluster}}-guestbook", Finalizers: []string{"resources-finalizer.argocd.argoproj.io/background"}, @@ -1221,7 +1221,7 @@ func TestCustomApplicationFinalizersGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ Name: "{{.cluster}}-guestbook", Finalizers: []string{"resources-finalizer.argocd.argoproj.io/background"}, @@ -1323,7 +1323,7 @@ func TestSimplePullRequestGenerator(t *testing.T) { Name: "simple-pull-request-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "guestbook-{{ number }}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -1400,10 +1400,8 @@ func TestSimplePullRequestGeneratorGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "guestbook-{{ .number }}", - Labels: map[string]string{"app": "{{index .labels 0}}"}}, + Template: &v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "guestbook-{{ .number }}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", Source: argov1alpha1.ApplicationSource{ @@ -1479,7 +1477,7 @@ func TestGitGeneratorPrivateRepo(t *testing.T) { Name: "simple-git-generator-private", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -1555,7 +1553,7 @@ func TestGitGeneratorPrivateRepoGoTemplate(t *testing.T) { }, Spec: v1alpha1.ApplicationSetSpec{ GoTemplate: true, - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", diff --git a/test/e2e/cluster_generator_test.go b/test/e2e/cluster_generator_test.go index dd3b98000bb31..add7f789ee0d3 100644 --- a/test/e2e/cluster_generator_test.go +++ b/test/e2e/cluster_generator_test.go @@ -49,7 +49,7 @@ func TestSimpleClusterGenerator(t *testing.T) { Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -173,7 +173,7 @@ func TestClusterGeneratorWithLocalCluster(t *testing.T) { Name: "in-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -264,7 +264,7 @@ func TestSimpleClusterGeneratorAddingCluster(t *testing.T) { Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -347,7 +347,7 @@ func TestSimpleClusterGeneratorDeletingCluster(t *testing.T) { Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", diff --git a/test/e2e/clusterdecisiongenerator_e2e_test.go b/test/e2e/clusterdecisiongenerator_e2e_test.go index 5a352f191e1d4..9393b52ea28be 100644 --- a/test/e2e/clusterdecisiongenerator_e2e_test.go +++ b/test/e2e/clusterdecisiongenerator_e2e_test.go @@ -61,7 +61,7 @@ func TestSimpleClusterDecisionResourceGenerator(t *testing.T) { Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -172,7 +172,7 @@ func TestSimpleClusterDecisionResourceGeneratorAddingCluster(t *testing.T) { Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -268,7 +268,7 @@ func TestSimpleClusterDecisionResourceGeneratorDeletingClusterSecret(t *testing. Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -372,7 +372,7 @@ func TestSimpleClusterDecisionResourceGeneratorDeletingClusterFromResource(t *te Name: "simple-cluster-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-guestbook"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", diff --git a/test/e2e/matrix_e2e_test.go b/test/e2e/matrix_e2e_test.go index fe084806d8803..f313ff66d2e0b 100644 --- a/test/e2e/matrix_e2e_test.go +++ b/test/e2e/matrix_e2e_test.go @@ -60,7 +60,7 @@ func TestListMatrixGenerator(t *testing.T) { Name: "matrix-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{values.name}}-{{path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -186,7 +186,7 @@ func TestClusterMatrixGenerator(t *testing.T) { Name: "matrix-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-{{path.basename}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", diff --git a/test/e2e/merge_e2e_test.go b/test/e2e/merge_e2e_test.go index 96ab00621edb2..2e81de521e50d 100644 --- a/test/e2e/merge_e2e_test.go +++ b/test/e2e/merge_e2e_test.go @@ -56,7 +56,7 @@ func TestListMergeGenerator(t *testing.T) { Name: "merge-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{path.basename}}-{{name-suffix}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", @@ -183,7 +183,7 @@ func TestClusterMergeGenerator(t *testing.T) { Name: "merge-generator", }, Spec: v1alpha1.ApplicationSetSpec{ - Template: v1alpha1.ApplicationSetTemplate{ + Template: &v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{name}}-{{path.basename}}-{{values.name-suffix}}"}, Spec: argov1alpha1.ApplicationSpec{ Project: "default", diff --git a/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx b/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx index b30600368f404..65edd42db41ff 100644 --- a/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx +++ b/ui/src/app/applications/components/application-operation-state/application-operation-state.tsx @@ -78,7 +78,7 @@ export const ApplicationOperationState: React.StatelessComponent = ({appl const confirmed = await ctx.apis.popup.confirm('Terminate operation', 'Are you sure you want to terminate operation?'); if (confirmed) { try { - await services.applications.terminateOperation(application.metadata.name); + await services.applications.terminateOperation(application.metadata.name, application.metadata.namespace); } catch (e) { ctx.apis.notifications.show({ content: , diff --git a/ui/src/app/shared/services/applications-service.ts b/ui/src/app/shared/services/applications-service.ts index c4d244f957726..91ae3aa061d87 100644 --- a/ui/src/app/shared/services/applications-service.ts +++ b/ui/src/app/shared/services/applications-service.ts @@ -387,6 +387,7 @@ export class ApplicationsService { public terminateOperation(applicationName: string, appNamespace: string): Promise { return requests .delete(`/applications/${applicationName}/operation`) + .query({appNamespace}) .send() .then(() => true); } diff --git a/util/helm/client.go b/util/helm/client.go index 7ef14e2cba36c..ec0c0bea4dbaa 100644 --- a/util/helm/client.go +++ b/util/helm/client.go @@ -390,10 +390,10 @@ func getTagsListURL(rawURL string, chart string) (string, error) { if err != nil { return "", fmt.Errorf("unable to parse repo url: %v", err) } + tagsPathFormat := "%s/v2/%s/tags/list" repoURL.Scheme = "https" - tagsList := strings.Join([]string{"v2", url.PathEscape(chart), "tags/list"}, "/") - repoURL.Path = strings.Join([]string{repoURL.Path, tagsList}, "/") - repoURL.RawPath = strings.Join([]string{repoURL.RawPath, tagsList}, "/") + repoURL.Path = fmt.Sprintf(tagsPathFormat, repoURL.Path, chart) + repoURL.RawPath = fmt.Sprintf(tagsPathFormat, repoURL.RawPath, url.PathEscape(chart)) return repoURL.String(), nil } @@ -406,7 +406,7 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) { allTags := &TagsList{} var data []byte for nextURL != "" { - log.Debugf("fetching %s tags from %s", chart, text.Trunc(nextURL, 100)) + log.Debugf("fetching %s tags from %s", chart, sanitizeLog(text.Trunc(nextURL, 100))) data, nextURL, err = c.getTagsFromUrl(nextURL) if err != nil { return nil, fmt.Errorf("failed tags part: %v", err) @@ -426,14 +426,30 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) { return data, nil } -func getNextUrl(linkHeader string) string { - nextUrl := "" - if linkHeader != "" { - // drop < >; ref= from the Link header, see: https://docs.docker.com/registry/spec/api/#pagination - nextUrl = strings.Split(linkHeader, ";")[0][1:] - nextUrl = nextUrl[:len(nextUrl)-1] +func getNextUrl(resp *http.Response) (string, error) { + link := resp.Header.Get("Link") + if link == "" { + return "", nil } - return nextUrl + if link[0] != '<' { + return "", fmt.Errorf("invalid next link %q: missing '<'", link) + } + if i := strings.IndexByte(link, '>'); i == -1 { + return "", fmt.Errorf("invalid next link %q: missing '>'", link) + } else { + link = link[1:i] + } + linkURL, err := resp.Request.URL.Parse(link) + if err != nil { + return "", err + } + return linkURL.String(), nil +} + +func sanitizeLog(input string) string { + sanitized := strings.ReplaceAll(input, "\r", "") + sanitized = strings.ReplaceAll(sanitized, "\n", "") + return sanitized } func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error) { @@ -484,8 +500,8 @@ func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error) if err != nil { return nil, "", fmt.Errorf("failed to read body: %v", err) } - nextUrl := getNextUrl(resp.Header.Get("Link")) - return data, nextUrl, nil + nextUrl, err := getNextUrl(resp) + return data, nextUrl, err } func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) { diff --git a/util/helm/client_test.go b/util/helm/client_test.go index 8f12eaf403c9e..8107a586e2772 100644 --- a/util/helm/client_test.go +++ b/util/helm/client_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" + "net/url" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -177,11 +178,34 @@ func TestGetTagsFromUrl(t *testing.T) { } func Test_getNextUrl(t *testing.T) { - nextUrl := getNextUrl("") + baseUrl, err := url.Parse("https://my.repo.com/v2/chart/tags/list") + if err != nil { + t.Errorf("failed to parse url in test case: %v", err) + } + resp := &http.Response{ + Request: &http.Request{ + URL: baseUrl, + }, + } + nextUrl, err := getNextUrl(resp) assert.Equal(t, nextUrl, "") + assert.NoError(t, err) + + var nextUrlAbsolute = "https://my.repo.com/v2/chart/tags/list?n=123&orderby=" + resp.Header = http.Header{ + "Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlAbsolute)}, + } + nextUrl, err = getNextUrl(resp) + assert.NoError(t, err) + assert.Equal(t, nextUrl, nextUrlAbsolute) - nextUrl = getNextUrl("; rel=next") - assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?token=123") + var nextUrlRelative = "/v2/chart/tags/list?n=123&orderby=" + resp.Header = http.Header{ + "Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlRelative)}, + } + nextUrl, err = getNextUrl(resp) + assert.NoError(t, err) + assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?n=123&orderby=") } func Test_getTagsListURL(t *testing.T) { @@ -197,4 +221,14 @@ func Test_getTagsListURL(t *testing.T) { tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "dss") assert.Nil(t, err) assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/dss/tags/list") + + // with unescaped characters allowed by https://www.rfc-editor.org/rfc/rfc3986#page-50 + tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts.-_~$&+=:@dss") + assert.Nil(t, err) + assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts.-_~$&+=:@dss/tags/list") + + // with escaped characters not allowed in path by https://www.rfc-editor.org/rfc/rfc3986#page-50 + tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts%/dss") + assert.Nil(t, err) + assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts%25%2Fdss/tags/list") }