diff --git a/commands/generate.go b/commands/generate.go index e5a2e4c57..981797128 100644 --- a/commands/generate.go +++ b/commands/generate.go @@ -8,13 +8,13 @@ import ( "fmt" "os" "sort" + "strings" "github.com/openfaas/faas-cli/builder" v2 "github.com/openfaas/faas-cli/schema/store/v2" "github.com/openfaas/faas-cli/proxy" "github.com/openfaas/faas-cli/schema" - knativev1 "github.com/openfaas/faas-cli/schema/knative/v1" openfaasv1 "github.com/openfaas/faas-cli/schema/openfaas/v1" "github.com/openfaas/faas-cli/util" "github.com/openfaas/go-sdk/stack" @@ -29,6 +29,8 @@ const ( defaultAPIVersion = "openfaas.com/v1" ) +var apiVersions = []string{"openfaas.com/v1", "openfaas.com/v2alpha2"} + var ( api string name string @@ -36,6 +38,7 @@ var ( crdFunctionNamespace string fromStore string desiredArch string + outputFormat string annotationArgs []string labelArgs []string ) @@ -43,9 +46,10 @@ var ( func init() { generateCmd.Flags().StringVar(&fromStore, "from-store", "", "generate using a store image") - generateCmd.Flags().StringVar(&name, "name", "", "for use with --from-store, override the name for the Function CR") + generateCmd.Flags().StringVar(&name, "name", "", "for use with --from-store, override the name for the generated function") + generateCmd.Flags().StringVar(&outputFormat, "output", "", "output format e.g stack.yaml, for use with --from-store to generate an OpenFaaS stack.yaml") - generateCmd.Flags().StringVar(&api, "api", defaultAPIVersion, "CRD API version e.g openfaas.com/v1, serving.knative.dev/v1") + generateCmd.Flags().StringVar(&api, "api", defaultAPIVersion, "CRD API version e.g openfaas.com/v1, openfaas.com/v2alpha2") generateCmd.Flags().StringVarP(&crdFunctionNamespace, "namespace", "n", "openfaas-fn", "Kubernetes namespace for functions") generateCmd.Flags().Var(&tagFormat, "tag", "Override latest tag on function Docker image, accepts 'digest', 'latest', 'sha', 'branch', 'describe'") generateCmd.Flags().BoolVar(&envsubst, "envsubst", true, "Substitute environment variables in stack.yaml file") @@ -62,19 +66,41 @@ var generateCmd = &cobra.Command{ Long: `The generate command creates kubernetes CRD YAML file for functions`, Example: ` faas-cli generate --api=openfaas.com/v1 --yaml stack.yaml | kubectl apply -f - faas-cli generate --api=openfaas.com/v1 -f stack.yaml - faas-cli generate --api=serving.knative.dev/v1 -f stack.yaml faas-cli generate --api=openfaas.com/v1 --namespace openfaas-fn -f stack.yaml - faas-cli generate --api=openfaas.com/v1 -f stack.yaml --tag branch -n openfaas-fn`, + faas-cli generate --api=openfaas.com/v1 -f stack.yaml --tag branch -n openfaas-fn + faas-cli generate --from-store nodeinfo --output stack.yaml > stack.yaml`, PreRunE: preRunGenerate, RunE: runGenerate, } func preRunGenerate(cmd *cobra.Command, args []string) error { + if outputFormat != "" && !isStackOutput(outputFormat) { + return fmt.Errorf("unsupported output format %q, must be one of: stack.yaml, stack", outputFormat) + } + + if isStackOutput(outputFormat) { + if len(fromStore) == 0 { + return fmt.Errorf("--output %s can only be used with --from-store", outputFormat) + } + + return nil + } + if len(api) == 0 { return fmt.Errorf("you must supply the API version with the --api flag") } - return nil + for _, version := range apiVersions { + if api == version { + return nil + } + } + + return fmt.Errorf("unsupported API version %q, must be one of: %s", api, strings.Join(apiVersions, ", ")) +} + +func isStackOutput(format string) bool { + return format == "stack.yaml" || format == "stack" } func filterStoreItem(items []v2.StoreFunction, fromStore string) (*v2.StoreFunction, error) { @@ -190,6 +216,16 @@ Alternatively, to generate a definition for store functions, use "--from-store"` os.Exit(1) } + if isStackOutput(outputFormat) { + stackYAML, err := generateStackYAML(services, gateway) + if err != nil { + return err + } + + fmt.Println(stackYAML) + return nil + } + objectsString, err := generateCRDYAML(services, tagFormat, api, crdFunctionNamespace, builder.NewFunctionMetadataSourceLive()) if err != nil { @@ -202,6 +238,63 @@ Alternatively, to generate a definition for store functions, use "--from-store"` return nil } +// stackYAML is the output format for --output stack.yaml, mirroring an +// OpenFaaS stack.yaml with only the values known from the store item +type stackYAML struct { + Version string `yaml:"version"` + Provider stack.Provider `yaml:"provider"` + Functions map[string]stackYAMLFn `yaml:"functions"` +} + +type stackYAMLFn struct { + Image string `yaml:"image"` + SkipBuild bool `yaml:"skip_build"` + Environment map[string]string `yaml:"environment,omitempty"` + Labels *map[string]string `yaml:"labels,omitempty"` + Annotations *map[string]string `yaml:"annotations,omitempty"` +} + +// generateStackYAML generates an OpenFaaS stack.yaml for functions, with +// skip_build set so the pre-built image is deployed without a rebuild +func generateStackYAML(services stack.Services, gateway string) (string, error) { + functions := make(map[string]stackYAMLFn, len(services.Functions)) + for name, function := range services.Functions { + if function.Labels != nil && len(*function.Labels) == 0 { + function.Labels = nil + } + + if function.Annotations != nil && len(*function.Annotations) == 0 { + function.Annotations = nil + } + + functions[name] = stackYAMLFn{ + Image: function.Image, + SkipBuild: true, + Environment: function.Environment, + Labels: function.Labels, + Annotations: function.Annotations, + } + } + + output := stackYAML{ + Version: "1.0", + Provider: stack.Provider{ + Name: "openfaas", + GatewayURL: gateway, + }, + Functions: functions, + } + + var buff bytes.Buffer + yamlEncoder := yaml.NewEncoder(&buff) + yamlEncoder.SetIndent(2) + if err := yamlEncoder.Encode(&output); err != nil { + return "", err + } + + return buff.String(), nil +} + // generateCRDYAML generates CRD YAML for functions func generateCRDYAML(services stack.Services, format schema.BuildFormat, apiVersion, namespace string, metadataSource builder.FunctionMetadataSource) (string, error) { @@ -209,10 +302,6 @@ func generateCRDYAML(services stack.Services, format schema.BuildFormat, apiVers if len(services.Functions) > 0 { - if apiVersion == knativev1.APIVersionLatest { - return generateknativev1ServingServiceCRDYAML(services, format, api, crdFunctionNamespace) - } - orderedNames := generateFunctionOrder(services.Functions) for _, name := range orderedNames { @@ -274,103 +363,6 @@ func generateCRDYAML(services stack.Services, format schema.BuildFormat, apiVers return objectsString, nil } -func generateknativev1ServingServiceCRDYAML(services stack.Services, format schema.BuildFormat, apiVersion, namespace string) (string, error) { - crds := []knativev1.ServingServiceCRD{} - - orderedNames := generateFunctionOrder(services.Functions) - - for _, name := range orderedNames { - - function := services.Functions[name] - - fileEnvironment, err := readFiles(function.EnvironmentFile) - if err != nil { - return "", err - } - - //combine all environment variables - allEnvironment, envErr := compileEnvironment([]string{}, function.Environment, fileEnvironment) - if envErr != nil { - return "", envErr - } - - env := orderknativeEnv(allEnvironment) - - var annotations map[string]string - - if function.Annotations != nil { - annotations = *function.Annotations - } - - branch, version, err := builder.GetImageTagValues(tagFormat, function.Handler) - if err != nil { - return "", err - } - - imageName := schema.BuildImageName(format, function.Image, version, branch) - - crd := knativev1.ServingServiceCRD{ - Metadata: schema.Metadata{ - Name: name, - Namespace: namespace, - Annotations: annotations, - }, - APIVersion: apiVersion, - Kind: "Service", - - Spec: knativev1.ServingServiceSpec{ - ServingServiceSpecTemplate: knativev1.ServingServiceSpecTemplate{ - Template: knativev1.ServingServiceSpecTemplateSpec{ - Containers: []knativev1.ServingSpecContainersContainerSpec{}, - }, - }, - }, - } - - crd.Spec.Template.Containers = append(crd.Spec.Template.Containers, knativev1.ServingSpecContainersContainerSpec{ - Image: imageName, - Env: env, - }) - - var mounts []knativev1.VolumeMount - var volumes []knativev1.Volume - - for _, secret := range function.Secrets { - mounts = append(mounts, knativev1.VolumeMount{ - MountPath: "/var/openfaas/secrets/" + secret, - ReadOnly: true, - Name: secret, - }) - volumes = append(volumes, knativev1.Volume{ - Name: secret, - Secret: knativev1.Secret{ - SecretName: secret, - }, - }) - } - - crd.Spec.Template.Volumes = volumes - crd.Spec.Template.Containers[0].VolumeMounts = mounts - - crds = append(crds, crd) - } - - var objectsString string - for _, crd := range crds { - - var buff bytes.Buffer - yamlEncoder := yaml.NewEncoder(&buff) - yamlEncoder.SetIndent(2) // this is what you're looking for - if err := yamlEncoder.Encode(&crd); err != nil { - return "", err - } - - objectsString += "---\n" + string(buff.Bytes()) - } - - return objectsString, nil -} - func generateFunctionOrder(functions map[string]stack.Function) []string { var functionNames []string @@ -383,21 +375,3 @@ func generateFunctionOrder(functions map[string]stack.Function) []string { return functionNames } - -func orderknativeEnv(environment map[string]string) []knativev1.EnvPair { - - var orderedEnvironment []string - var envVars []knativev1.EnvPair - - for k := range environment { - orderedEnvironment = append(orderedEnvironment, k) - } - - sort.Strings(orderedEnvironment) - - for _, envVar := range orderedEnvironment { - envVars = append(envVars, knativev1.EnvPair{Name: envVar, Value: environment[envVar]}) - } - - return envVars -} diff --git a/commands/generate_test.go b/commands/generate_test.go index 68b9b2cbe..a667e0291 100644 --- a/commands/generate_test.go +++ b/commands/generate_test.go @@ -539,3 +539,176 @@ func Test_generateFunctionOrder(t *testing.T) { } } + +func Test_preRunGenerate_API(t *testing.T) { + testcases := []struct { + Name string + API string + Expected bool + }{ + { + Name: "Empty API version", + API: "", + Expected: false, + }, + { + Name: "OpenFaaS v1 API version", + API: "openfaas.com/v1", + Expected: true, + }, + { + Name: "OpenFaaS v2alpha2 API version", + API: "openfaas.com/v2alpha2", + Expected: true, + }, + { + Name: "Knative API version", + API: "serving.knative.dev/v1", + Expected: false, + }, + { + Name: "Unknown API version", + API: "openfaas.com/v2", + Expected: false, + }, + } + + for _, testcase := range testcases { + api = testcase.API + err := preRunGenerate(generateCmd, []string{}) + + if testcase.Expected && err != nil { + t.Fatalf("%s: expected success, got error: %s", testcase.Name, err) + } + + if !testcase.Expected && err == nil { + t.Fatalf("%s: expected error, got nil", testcase.Name) + } + } + + api = defaultAPIVersion +} + +func Test_generateStackYAML(t *testing.T) { + labels := map[string]string{"colour": "blue"} + annotations := map[string]string{"topic": "oauth"} + + services := stack.Services{ + Functions: map[string]stack.Function{ + "nodeinfo": { + Name: "nodeinfo", + Image: "ghcr.io/openfaas/nodeinfo:latest", + Environment: map[string]string{"fprocess": "node index.js"}, + Labels: &labels, + Annotations: &annotations, + }, + }, + } + + generatedYAML, err := generateStackYAML(services, "https://gateway.example.com") + if err != nil { + t.Fatalf("failed to generate stack YAML: %s", err) + } + + parsed, err := stack.ParseYAMLData([]byte(generatedYAML), "", "", true) + if err != nil { + t.Fatalf("failed to parse generated stack YAML: %s", err) + } + if parsed == nil { + t.Fatal("generated stack YAML was empty") + } + + if parsed.Version != "1.0" { + t.Errorf("want version 1.0, got %q", parsed.Version) + } + + if parsed.Provider.Name != "openfaas" { + t.Errorf("want provider name openfaas, got %q", parsed.Provider.Name) + } + + if parsed.Provider.GatewayURL != "https://gateway.example.com" { + t.Errorf("want gateway https://gateway.example.com, got %q", parsed.Provider.GatewayURL) + } + + function, ok := parsed.Functions["nodeinfo"] + if !ok { + t.Fatal("want function nodeinfo in generated stack YAML") + } + + if !function.SkipBuild { + t.Error("want skip_build to be true") + } + + if function.Image != "ghcr.io/openfaas/nodeinfo:latest" { + t.Errorf("want image ghcr.io/openfaas/nodeinfo:latest, got %q", function.Image) + } + + if function.Environment["fprocess"] != "node index.js" { + t.Errorf("want fprocess node index.js, got %q", function.Environment["fprocess"]) + } + + if function.Labels == nil || (*function.Labels)["colour"] != "blue" { + t.Errorf("want label colour: blue, got %v", function.Labels) + } + + if function.Annotations == nil || (*function.Annotations)["topic"] != "oauth" { + t.Errorf("want annotation topic: oauth, got %v", function.Annotations) + } +} + +func Test_preRunGenerate_Output(t *testing.T) { + testcases := []struct { + Name string + Output string + FromStore string + ExpectError bool + }{ + { + Name: "stack.yaml output with --from-store", + Output: "stack.yaml", + FromStore: "nodeinfo", + ExpectError: false, + }, + { + Name: "stack alias with --from-store", + Output: "stack", + FromStore: "nodeinfo", + ExpectError: false, + }, + { + Name: "stack.yaml output without --from-store", + Output: "stack.yaml", + FromStore: "", + ExpectError: true, + }, + { + Name: "unknown output format", + Output: "json", + FromStore: "", + ExpectError: true, + }, + { + Name: "no output flag", + Output: "", + FromStore: "", + ExpectError: false, + }, + } + + for _, testcase := range testcases { + outputFormat = testcase.Output + fromStore = testcase.FromStore + + err := preRunGenerate(generateCmd, []string{}) + if testcase.ExpectError && err == nil { + t.Fatalf("%s: expected error, got nil", testcase.Name) + } + + if !testcase.ExpectError && err != nil { + t.Fatalf("%s: expected success, got error: %s", testcase.Name, err) + } + } + + outputFormat = "" + fromStore = "" +} diff --git a/schema/knative/v1/serving.go b/schema/knative/v1/serving.go deleted file mode 100644 index be0cfea2b..000000000 --- a/schema/knative/v1/serving.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) OpenFaaS Author(s) 2019. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -package v1 - -import "github.com/openfaas/faas-cli/schema" - -const APIVersionLatest = "serving.knative.dev/v1" - -// ServingServiceCRD root level YAML definition for the object -type ServingServiceCRD struct { - //APIVersion CRD API version - APIVersion string `yaml:"apiVersion"` - //Kind kind of the object - Kind string `yaml:"kind"` - Metadata schema.Metadata `yaml:"metadata,omitempty"` - Spec ServingServiceSpec `yaml:"spec"` -} - -type ServingServiceSpec struct { - ServingServiceSpecTemplate `yaml:"template"` -} - -type ServingServiceSpecTemplateSpec struct { - Containers []ServingSpecContainersContainerSpec `yaml:"containers"` - Volumes []Volume `yaml:"volumes,omitempty"` -} -type ServingServiceSpecTemplate struct { - Template ServingServiceSpecTemplateSpec `yaml:"spec"` -} - -type ServingSpecContainersContainerSpec struct { - Image string `yaml:"image"` - Env []EnvPair `yaml:"env,omitempty"` - VolumeMounts []VolumeMount `yaml:"volumeMounts,omitempty"` -} - -type VolumeMount struct { - Name string `yaml:"name"` - MountPath string `yaml:"mountPath"` - ReadOnly bool `yaml:"readOnly"` -} - -type Volume struct { - Name string `yaml:"name"` - Secret Secret `yaml:"secret"` -} - -type Secret struct { - SecretName string `yaml:"secretName"` -} - -type EnvPair struct { - Name string `yaml:"name"` - Value string `yaml:"value"` -}