diff --git a/VERSION.txt b/VERSION.txt index 8a9ecc2ea..7bcd0e361 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.0.2 \ No newline at end of file diff --git a/pkg/cmd/accept/cmd.go b/pkg/cmd/accept/cmd.go index ed3b78c2e..4b337a808 100644 --- a/pkg/cmd/accept/cmd.go +++ b/pkg/cmd/accept/cmd.go @@ -16,10 +16,6 @@ var example = ` %[1]s accept --clusters ,,... ` -const ( - scenarioDirectory = "accept" -) - // NewCmd ... func NewCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := newOptions(f, streams) diff --git a/pkg/cmd/init/cmd.go b/pkg/cmd/init/cmd.go index 6eda25488..282d4dd83 100644 --- a/pkg/cmd/init/cmd.go +++ b/pkg/cmd/init/cmd.go @@ -16,10 +16,6 @@ var example = ` %[1]s init ` -const ( - scenarioDirectory = "init" -) - // NewCmd ... func NewCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := newOptions(f, streams) diff --git a/pkg/cmd/init/exec.go b/pkg/cmd/init/exec.go index c6148ea63..925cdaf29 100644 --- a/pkg/cmd/init/exec.go +++ b/pkg/cmd/init/exec.go @@ -3,10 +3,12 @@ package init import ( "fmt" + "time" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" "open-cluster-management.io/clusteradm/pkg/cmd/init/scenario" "open-cluster-management.io/clusteradm/pkg/helpers" + "open-cluster-management.io/clusteradm/pkg/helpers/apply" "github.com/spf13/cobra" @@ -61,17 +63,21 @@ func (o *Options) run() error { "init/service_account.yaml", } - err = helpers.ApplyDirectly(clientHolder, reader, scenarioDirectory, o.values, files...) + err = apply.ApplyDirectly(clientHolder, reader, o.values, "", files...) if err != nil { return err } - err = helpers.ApplyDeployment(kubeClient, reader, scenarioDirectory, o.values, "init/operator.yaml") + err = apply.ApplyDeployment(kubeClient, reader, o.values, "", "init/operator.yaml") if err != nil { return err } + //quick fix for https://github.com/open-cluster-management-io/clusteradm/issues/12 + fmt.Printf("Wait 10 sec... for the crd to be effective\n") + time.Sleep(10 * time.Second) + discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(restConfig) - err = helpers.ApplyCustomResouces(dynamicClient, discoveryClient, reader, scenarioDirectory, o.values, "init/clustermanagers.cr.yaml") + err = apply.ApplyCustomResouces(dynamicClient, discoveryClient, reader, o.values, "", "init/clustermanagers.cr.yaml") if err != nil { return err } diff --git a/pkg/cmd/init/scenario/resources.go b/pkg/cmd/init/scenario/resources.go index 7f91f0212..149839db1 100644 --- a/pkg/cmd/init/scenario/resources.go +++ b/pkg/cmd/init/scenario/resources.go @@ -4,12 +4,12 @@ package scenario import ( "embed" - "open-cluster-management.io/clusteradm/pkg/helpers" + "open-cluster-management.io/clusteradm/pkg/helpers/asset" ) //go:embed init var files embed.FS -func GetScenarioResourcesReader() *helpers.ScenarioResourcesReader { - return helpers.NewScenarioResourcesReader(&files) +func GetScenarioResourcesReader() *asset.ScenarioResourcesReader { + return asset.NewScenarioResourcesReader(&files) } diff --git a/pkg/cmd/join/cmd.go b/pkg/cmd/join/cmd.go index df626cdbd..b3603d1a3 100644 --- a/pkg/cmd/join/cmd.go +++ b/pkg/cmd/join/cmd.go @@ -16,10 +16,6 @@ var example = ` %[1]s join --hub-token --hub-apiserver --name ` -const ( - scenarioDirectory = "join" -) - // NewCmd ... func NewCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := newOptions(f, streams) diff --git a/pkg/cmd/join/exec.go b/pkg/cmd/join/exec.go index b56ad5cd7..180d973d0 100644 --- a/pkg/cmd/join/exec.go +++ b/pkg/cmd/join/exec.go @@ -3,6 +3,7 @@ package join import ( "fmt" + "time" "github.com/ghodss/yaml" "github.com/openshift/library-go/pkg/operator/resource/resourceapply" @@ -14,6 +15,7 @@ import ( clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1" "open-cluster-management.io/clusteradm/pkg/cmd/join/scenario" "open-cluster-management.io/clusteradm/pkg/helpers" + "open-cluster-management.io/clusteradm/pkg/helpers/apply" "github.com/spf13/cobra" ) @@ -90,18 +92,22 @@ func (o *Options) run() error { "join/service_account.yaml", } - err = helpers.ApplyDirectly(clientHolder, reader, scenarioDirectory, o.values, files...) + err = apply.ApplyDirectly(clientHolder, reader, o.values, "", files...) if err != nil { return err } - err = helpers.ApplyDeployment(kubeClient, reader, scenarioDirectory, o.values, "join/operator.yaml") + err = apply.ApplyDeployment(kubeClient, reader, o.values, "", "join/operator.yaml") if err != nil { return err } + //quick fix for https://github.com/open-cluster-management-io/clusteradm/issues/12 + fmt.Printf("Wait 10 sec... for the crd to be effective\n") + time.Sleep(10 * time.Second) + discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(restConfig) - err = helpers.ApplyCustomResouces(dynamicClient, discoveryClient, reader, scenarioDirectory, o.values, "join/klusterlets.cr.yaml") + err = apply.ApplyCustomResouces(dynamicClient, discoveryClient, reader, o.values, "", "join/klusterlets.cr.yaml") if err != nil { return err } diff --git a/pkg/cmd/join/scenario/resources.go b/pkg/cmd/join/scenario/resources.go index 7bc63dc86..71dc9cafe 100644 --- a/pkg/cmd/join/scenario/resources.go +++ b/pkg/cmd/join/scenario/resources.go @@ -4,12 +4,12 @@ package scenario import ( "embed" - "open-cluster-management.io/clusteradm/pkg/helpers" + "open-cluster-management.io/clusteradm/pkg/helpers/asset" ) //go:embed join var files embed.FS -func GetScenarioResourcesReader() *helpers.ScenarioResourcesReader { - return helpers.NewScenarioResourcesReader(&files) +func GetScenarioResourcesReader() *asset.ScenarioResourcesReader { + return asset.NewScenarioResourcesReader(&files) } diff --git a/pkg/helpers/apply.go b/pkg/helpers/apply/apply.go similarity index 66% rename from pkg/helpers/apply.go rename to pkg/helpers/apply/apply.go index 3204e4662..3736d07c1 100644 --- a/pkg/helpers/apply.go +++ b/pkg/helpers/apply/apply.go @@ -1,16 +1,19 @@ // Copyright Contributors to the Open Cluster Management project -package helpers +package apply import ( "bytes" "context" "fmt" + "regexp" "strings" "text/template" "github.com/Masterminds/sprig" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "open-cluster-management.io/clusteradm/pkg/helpers" + "open-cluster-management.io/clusteradm/pkg/helpers/asset" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -27,6 +30,10 @@ import ( "github.com/openshift/library-go/pkg/operator/resource/resourceapply" ) +const ( + ErrorEmptyAssetAfterTemplating = "ERROR_EMPTY_ASSET_AFTER_TEMPLATING" +) + var ( genericScheme = runtime.NewScheme() genericCodecs = serializer.NewCodecFactory(genericScheme) @@ -35,15 +42,18 @@ var ( func ApplyDeployment( client kubernetes.Interface, - reader ScenarioReader, - templateName string, + reader asset.ScenarioReader, values interface{}, + headerFile string, files ...string) error { genericScheme.AddKnownTypes(appsv1.SchemeGroupVersion, &appsv1.Deployment{}) - recorder := events.NewInMemoryRecorder(GetExampleHeader()) + recorder := events.NewInMemoryRecorder(helpers.GetExampleHeader()) for _, name := range files { - deploymentBytes, err := MustTempalteAsset(name, templateName, reader, values) + deploymentBytes, err := MustTempalteAsset(name, headerFile, reader, values) if err != nil { + if IsEmptyAsset(err) { + continue + } return err } deployment, sch, err := genericCodec.Decode(deploymentBytes, nil, nil) @@ -62,16 +72,16 @@ func ApplyDeployment( } func ApplyDirectly(clients *resourceapply.ClientHolder, - reader ScenarioReader, - templateName string, + reader asset.ScenarioReader, values interface{}, + headerFile string, files ...string) error { - recorder := events.NewInMemoryRecorder(GetExampleHeader()) + recorder := events.NewInMemoryRecorder(helpers.GetExampleHeader()) resourceResults := resourceapply.ApplyDirectly(clients, recorder, func(name string) ([]byte, error) { - return MustTempalteAsset(name, templateName, reader, values) + return MustTempalteAsset(name, headerFile, reader, values) }, files...) for _, result := range resourceResults { - if result.Error != nil { + if result.Error != nil && !IsEmptyAsset(result.Error) { return fmt.Errorf("%q (%T): %v", result.File, result.Type, result.Error) } } @@ -80,13 +90,16 @@ func ApplyDirectly(clients *resourceapply.ClientHolder, func ApplyCustomResouces(client dynamic.Interface, discoveryClient discovery.DiscoveryInterface, - reader ScenarioReader, - templateName string, + reader asset.ScenarioReader, values interface{}, + headerFile string, files ...string) error { for _, name := range files { - asset, err := MustTempalteAsset(name, templateName, reader, values) + asset, err := MustTempalteAsset(name, headerFile, reader, values) if err != nil { + if IsEmptyAsset(err) { + continue + } return err } u, err := bytesToUnstructured(reader, asset) @@ -122,18 +135,7 @@ func ApplyCustomResouces(client dynamic.Interface, return nil } -func getResource(kind string) (string, error) { - switch kind { - case "ClusterManager": - return "clustermanagers", nil - case "Klusterlet": - return "klusterlets", nil - default: - return "", fmt.Errorf("kind: %s not supported", kind) - } -} - -func bytesToUnstructured(reader ScenarioReader, asset []byte) (*unstructured.Unstructured, error) { +func bytesToUnstructured(reader asset.ScenarioReader, asset []byte) (*unstructured.Unstructured, error) { j, err := reader.ToJSON(asset) if err != nil { return nil, err @@ -159,8 +161,18 @@ func getTemplate(templateName string) *template.Template { return tmpl } -func MustTempalteAsset(name, templateName string, reader ScenarioReader, values interface{}) ([]byte, error) { - tmpl := getTemplate(templateName) +//MustTempalteAsset generates textual output for an file name. +//If a headerFile is specified it will be added as a header of the file. +func MustTempalteAsset(name, headerFile string, reader asset.ScenarioReader, values interface{}) ([]byte, error) { + tmpl := getTemplate(name) + h := []byte{} + var err error + if headerFile != "" { + h, err = reader.Asset(headerFile) + if err != nil { + return nil, err + } + } b, err := reader.Asset(name) if err != nil { return nil, err @@ -170,17 +182,34 @@ func MustTempalteAsset(name, templateName string, reader ScenarioReader, values if err != nil { return nil, err } + tmplParsed, err = tmplParsed.Parse(string(h)) + if err != nil { + return nil, err + } err = tmplParsed.Execute(&buf, values) if err != nil { return nil, err } - // recorder.Eventf("templated:\n%s\n---", buf.String()) - trim := strings.TrimSuffix(buf.String(), "\n") - trim = strings.TrimSpace(trim) - if len(trim) == 0 { - return nil, nil + if isEmpty(buf.Bytes()) { + return nil, fmt.Errorf("asset %s becomes %s", name, ErrorEmptyAssetAfterTemplating) } + return buf.Bytes(), nil } + +func isEmpty(body []byte) bool { + //Remove comments + re := regexp.MustCompile("#.*") + bodyNoComment := re.ReplaceAll(body, nil) + //Remove blank lines + trim := strings.TrimSuffix(string(bodyNoComment), "\n") + trim = strings.TrimSpace(trim) + + return len(trim) == 0 +} + +func IsEmptyAsset(err error) bool { + return strings.Contains(err.Error(), ErrorEmptyAssetAfterTemplating) +} diff --git a/pkg/helpers/apply/apply_test.go b/pkg/helpers/apply/apply_test.go new file mode 100644 index 000000000..35147f155 --- /dev/null +++ b/pkg/helpers/apply/apply_test.go @@ -0,0 +1,90 @@ +// Copyright Contributors to the Open Cluster Management project +package apply + +import ( + "reflect" + "testing" + + "open-cluster-management.io/clusteradm/pkg/helpers/asset" + "open-cluster-management.io/clusteradm/test/unit/resources/scenario" +) + +func TestMustTempalteAsset(t *testing.T) { + type args struct { + name string + headerFile string + reader asset.ScenarioReader + values interface{} + } + tests := []struct { + name string + args args + want []byte + wantErr bool + }{ + { + name: "success", + args: args{ + name: "musttemplateasset/body.txt", + headerFile: "", + reader: scenario.GetScenarioResourcesReader(), + values: map[string]string{"myvalue": "thevalue"}, + }, + want: []byte("# hello\nthevalue"), + wantErr: false, + }, + { + name: "success with header", + args: args{ + name: "musttemplateasset/body_for_header.txt", + headerFile: "musttemplateasset/header.txt", + reader: scenario.GetScenarioResourcesReader(), + values: map[string]string{"myvalue": "thevalue"}, + }, + want: []byte("thevalue\n\n\nhello"), + wantErr: false, + }, + { + name: "fails syntax error template", + args: args{ + name: "musttemplateasset/body_with_syntax_error.txt", + headerFile: "", + reader: scenario.GetScenarioResourcesReader(), + values: map[string]string{"myvalue": "thevalue"}, + }, + wantErr: true, + }, + { + name: "fails syntax error template with header", + args: args{ + name: "musttemplateasset/body_with_syntax_error.txt", + headerFile: "musttemplateasset/header.txt", + reader: scenario.GetScenarioResourcesReader(), + values: map[string]string{"myvalue": "thevalue"}, + }, + wantErr: true, + }, + { + name: "fails empty", + args: args{ + name: "musttemplateasset/body_empty.txt", + headerFile: "", + reader: scenario.GetScenarioResourcesReader(), + values: map[string]string{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MustTempalteAsset(tt.args.name, tt.args.headerFile, tt.args.reader, tt.args.values) + if (err != nil) != tt.wantErr { + t.Errorf("MustTempalteAsset() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("MustTempalteAsset() = \n%v\n, want \n%v", string(got), string(tt.want)) + } + }) + } +} diff --git a/pkg/helpers/templatefunction.go b/pkg/helpers/apply/templatefunction.go similarity index 96% rename from pkg/helpers/templatefunction.go rename to pkg/helpers/apply/templatefunction.go index eef7245c3..ed550cc32 100644 --- a/pkg/helpers/templatefunction.go +++ b/pkg/helpers/apply/templatefunction.go @@ -1,6 +1,6 @@ // Copyright Contributors to the Open Cluster Management project -package helpers +package apply import ( "bytes" @@ -46,7 +46,7 @@ func encodeBase64(s string) string { //TemplateFuncMap generates function map for "include" func TemplateFuncMap(tmpl *template.Template) (funcMap template.FuncMap) { - funcMap = make(template.FuncMap, 0) + funcMap = make(template.FuncMap) funcMap["include"] = func(name string, data interface{}) (string, error) { buf := bytes.NewBuffer(nil) if err := tmpl.ExecuteTemplate(buf, name, data); err != nil { diff --git a/pkg/helpers/embedFSReader.go b/pkg/helpers/asset/embedFSReader.go similarity index 99% rename from pkg/helpers/embedFSReader.go rename to pkg/helpers/asset/embedFSReader.go index 9747b5fee..3b2fc68d6 100644 --- a/pkg/helpers/embedFSReader.go +++ b/pkg/helpers/asset/embedFSReader.go @@ -1,6 +1,6 @@ // Copyright Contributors to the Open Cluster Management project -package helpers +package asset import ( "embed" diff --git a/pkg/helpers/cmd.go b/pkg/helpers/cmd.go index f4fa3e446..ea797c3fd 100644 --- a/pkg/helpers/cmd.go +++ b/pkg/helpers/cmd.go @@ -7,6 +7,7 @@ import ( "os" "github.com/spf13/cobra" + "open-cluster-management.io/clusteradm/pkg/helpers/asset" ) func GetExampleHeader() string { @@ -20,7 +21,7 @@ func GetExampleHeader() string { } } -func UsageTempate(cmd *cobra.Command, reader ScenarioReader, valuesTemplatePath string) string { +func UsageTempate(cmd *cobra.Command, reader asset.ScenarioReader, valuesTemplatePath string) string { baseUsage := cmd.UsageTemplate() b, err := reader.Asset(valuesTemplatePath) if err != nil { diff --git a/test/unit/resources/scenario/musttemplateasset/body.txt b/test/unit/resources/scenario/musttemplateasset/body.txt new file mode 100644 index 000000000..8d8f31061 --- /dev/null +++ b/test/unit/resources/scenario/musttemplateasset/body.txt @@ -0,0 +1,2 @@ +# hello +{{ .myvalue }} \ No newline at end of file diff --git a/test/unit/resources/scenario/musttemplateasset/body_empty.txt b/test/unit/resources/scenario/musttemplateasset/body_empty.txt new file mode 100644 index 000000000..958133612 --- /dev/null +++ b/test/unit/resources/scenario/musttemplateasset/body_empty.txt @@ -0,0 +1,4 @@ + # hello +{{ if false }} +hello +{{ end }} \ No newline at end of file diff --git a/test/unit/resources/scenario/musttemplateasset/body_for_header.txt b/test/unit/resources/scenario/musttemplateasset/body_for_header.txt new file mode 100644 index 000000000..9f34b3800 --- /dev/null +++ b/test/unit/resources/scenario/musttemplateasset/body_for_header.txt @@ -0,0 +1,2 @@ +{{ .myvalue }} +{{ include "myfunc" . }} \ No newline at end of file diff --git a/test/unit/resources/scenario/musttemplateasset/body_with_syntax_error.txt b/test/unit/resources/scenario/musttemplateasset/body_with_syntax_error.txt new file mode 100644 index 000000000..08096bf6f --- /dev/null +++ b/test/unit/resources/scenario/musttemplateasset/body_with_syntax_error.txt @@ -0,0 +1 @@ +{{ .myvalue hello }} \ No newline at end of file diff --git a/test/unit/resources/scenario/musttemplateasset/header.txt b/test/unit/resources/scenario/musttemplateasset/header.txt new file mode 100644 index 000000000..b8192f772 --- /dev/null +++ b/test/unit/resources/scenario/musttemplateasset/header.txt @@ -0,0 +1,4 @@ +{{- define "myfunc" }} +{{ $result := "hello" }} +{{ $result }} +{{- end }} \ No newline at end of file diff --git a/test/unit/resources/scenario/resources.go b/test/unit/resources/scenario/resources.go new file mode 100644 index 000000000..11f68ed82 --- /dev/null +++ b/test/unit/resources/scenario/resources.go @@ -0,0 +1,15 @@ +// Copyright Contributors to the Open Cluster Management project +package scenario + +import ( + "embed" + + "open-cluster-management.io/clusteradm/pkg/helpers/asset" +) + +//go:embed musttemplateasset +var files embed.FS + +func GetScenarioResourcesReader() *asset.ScenarioResourcesReader { + return asset.NewScenarioResourcesReader(&files) +}