internal/cmd/integrations/scaffold.go (479 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package integrations import ( "encoding/json" "errors" "fmt" "internal/apiclient" "internal/client/authconfigs" "internal/client/connections" "internal/client/integrations" "internal/client/sfdc" "internal/clilog" "internal/cmd/utils" "os" "path" "regexp" "slices" "strconv" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // ScaffoldCmd to publish an integration flow version var ScaffoldCmd = &cobra.Command{ Use: "scaffold", Short: "Create a scaffolding for the integration flow", Long: "Create a scaffolding for the integration flow and dependencies", Args: func(cmd *cobra.Command, args []string) (err error) { cmdProject := utils.GetStringParam(cmd.Flag("proj")) cmdRegion := utils.GetStringParam(cmd.Flag("reg")) version := utils.GetStringParam(cmd.Flag("ver")) userLabel := utils.GetStringParam(cmd.Flag("user-label")) snapshot := utils.GetStringParam(cmd.Flag("snapshot")) latest, _ := strconv.ParseBool(utils.GetStringParam(cmd.Flag("latest"))) if err = apiclient.SetRegion(cmdRegion); err != nil { return err } else if err = validate(version, userLabel, snapshot, latest); err != nil { return err } cmd.Flags().VisitAll(func(f *pflag.Flag) { clilog.Debug.Printf("%s: %s\n", f.Name, f.Value) }) return apiclient.SetProjectID(cmdProject) }, RunE: func(cmd *cobra.Command, args []string) (err error) { cmd.SilenceUsage = true const jsonExt = ".json" var fileSplitter string var integrationBody, overridesBody, testCasesBody []byte version := utils.GetStringParam(cmd.Flag("ver")) userLabel := utils.GetStringParam(cmd.Flag("user-label")) snapshot := utils.GetStringParam(cmd.Flag("snapshot")) name := utils.GetStringParam(cmd.Flag("name")) githubAction, _ := strconv.ParseBool(utils.GetStringParam(cmd.Flag("github-action"))) if useUnderscore { fileSplitter = utils.LegacyFileSplitter } else { fileSplitter = utils.DefaultFileSplitter } if folder != "" { if stat, err := os.Stat(folder); err != nil || !stat.IsDir() { return fmt.Errorf("problem with supplied path, %w", err) } } else { if folder, err = os.Getwd(); err != nil { return err } } baseFolder := folder if env != "" { folder = path.Join(folder, env) if err = generateFolder(folder); err != nil { return err } } latest := ignoreLatest(version, userLabel, snapshot) if latest { if version, err = getLatestVersion(name); err != nil { return err } } apiclient.DisableCmdPrintHttpResponse() // Get if version != "" { if integrationBody, err = integrations.Get(name, version, false, true, false); err != nil { return err } if overridesBody, err = integrations.Get(name, version, false, false, true); err != nil { return err } if testCasesBody, err = integrations.ListTestCases(name, version, false, "", -1, "", ""); err != nil { return err } } else if userLabel != "" { if integrationBody, err = integrations.GetByUserlabel(name, userLabel, false, true, false); err != nil { return err } if overridesBody, err = integrations.GetByUserlabel(name, userLabel, false, false, true); err != nil { return err } if testCasesBody, err = integrations.ListTestCasesByUserlabel(name, userLabel, false, "", -1, "", ""); err != nil { return err } } else if snapshot != "" { if integrationBody, err = integrations.GetBySnapshot(name, snapshot, false, true, false); err != nil { return err } if overridesBody, err = integrations.GetBySnapshot(name, snapshot, false, false, true); err != nil { return err } if testCasesBody, err = integrations.ListTestCasesBySnapshot(name, snapshot, false, "", -1, "", ""); err != nil { return err } } else { return errors.New("latest version not found. 1) The integration may be in DRAFT state. Pass a snapshot number. 2) An invalid integration name was set. 3) Latest flag was combined with version, snapshot or user-label") } clilog.Info.Printf("Storing the Integration: %s\n", name) if err = generateFolder(path.Join(baseFolder, "src")); err != nil { return err } integrationBody, err = apiclient.PrettifyJson(integrationBody) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(baseFolder, "src", name+jsonExt), false, integrationBody); err != nil { return err } if len(testCasesBody) > 3 { clilog.Info.Printf("Found test cases in the integration, storing the test cases file\n") if err = generateFolder(path.Join(folder, "tests")); err != nil { return err } if err = generateFolder(path.Join(folder, "test-configs")); err != nil { return err } if err = generateTestcases(testCasesBody, integrationBody, folder); err != nil { return err } } // write integration overrides if len(overridesBody) > 0 && string(overridesBody) != "{}" { clilog.Info.Printf("Found overrides in the integration, storing the overrides file\n") if err = generateFolder(path.Join(folder, "overrides")); err != nil { return err } overridesBody, err = apiclient.PrettifyJson(overridesBody) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(folder, "overrides", "overrides.json"), false, overridesBody); err != nil { return err } } // write integation config variables configVariables, err := integrations.GetConfigVariables(integrationBody) if err != nil { return err } if len(configVariables) > 0 { clilog.Info.Printf("Found config variables in the integration, storing the config file\n") if err = generateFolder(path.Join(folder, "config-variables")); err != nil { return err } configVariables, err = apiclient.PrettifyJson(configVariables) if err = apiclient.WriteByteArrayToFile( path.Join(folder, "config-variables", name+"-config.json"), false, configVariables); err != nil { return err } } // extract code if extractCode { codeMap, err := integrations.ExtractCode(integrationBody) if err != nil { return err } if len(codeMap["JavaScriptTask"]) > 0 { javascriptFolder := path.Join(baseFolder, "src", "javascript") if err = generateFolder(javascriptFolder); err != nil { return err } clilog.Info.Printf("Found JavaScript files in the integration; generating separate files\n") for taskId, taskContent := range codeMap["JavaScriptTask"] { if err = apiclient.WriteByteArrayToFile( path.Join(javascriptFolder, "javascript_"+string(taskId)+".js"), false, []byte(taskContent)); err != nil { return err } } } if len(codeMap["JsonnetMapperTask"]) > 0 { jsonnetFolder := path.Join(baseFolder, "src", "datatransformer") if err = generateFolder(jsonnetFolder); err != nil { return err } clilog.Info.Printf("Found Jsonnet files in the integration; generating separate files\n") for taskId, taskContent := range codeMap["JsonnetMapperTask"] { if err = apiclient.WriteByteArrayToFile( path.Join(jsonnetFolder, "datatransformer_"+string(taskId)+".jsonnet"), false, []byte(taskContent)); err != nil { return err } } } } // auth config authConfigUuids, err := integrations.GetAuthConfigs(integrationBody) if err != nil { return err } if !skipAuthconfigs { if len(authConfigUuids) > 0 { clilog.Info.Printf("Found authconfigs in the integration\n") if err = generateFolder(path.Join(folder, "authconfigs")); err != nil { return err } for _, authConfigUUIDs := range authConfigUuids { authConfigResp, err := authconfigs.Get(authConfigUUIDs, true) if err != nil { return err } authConfigName := getName(authConfigResp) clilog.Info.Printf("Storing authconfig %s\n", authConfigName) authConfigResp, err = apiclient.PrettifyJson(authConfigResp) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(folder, "authconfigs", authConfigName+jsonExt), false, authConfigResp); err != nil { return err } } } } else { clilog.Info.Printf("Skipping scaffold of authconfigs configuration\n") } if !skipConnectors { connectors, err := integrations.GetConnectionsWithRegion(integrationBody) if err != nil { return err } if len(connectors) > 0 { clilog.Info.Printf("Found connectors in the integration\n") if err = generateFolder(path.Join(folder, "connectors")); err != nil { return err } // check for custom connectors for _, connector := range connectors { if connector.CustomConnection { if err = generateFolder(path.Join(folder, "custom-connectors")); err != nil { return err } break } } for _, connector := range connectors { if connector.CustomConnection { customConnectionResp, err := connections.GetCustomVersion(connector.Name, connector.Version, true) if err != nil { return err } clilog.Info.Printf("Storing custom connector %s\n", connector.Name) customConnectionResp, err = apiclient.PrettifyJson(customConnectionResp) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(folder, "custom-connectors", connector.Name+fileSplitter+connector.Version+jsonExt), false, customConnectionResp); err != nil { return err } } else { connectionResp, err := connections.GetConnectionDetailWithRegion(connector.Name, connector.Region, "", true, true) if err != nil { return err } clilog.Info.Printf("Storing connector %s\n", connector.Name) connectionResp, err = apiclient.PrettifyJson(connectionResp) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(folder, "connectors", connector.Name+jsonExt), false, connectionResp); err != nil { return err } } } } } else { clilog.Info.Printf("Skipping scaffold of connector configuration\n") } instances, err := integrations.GetSfdcInstances(integrationBody) if err != nil { return err } if len(instances) > 0 { clilog.Info.Printf("Found sfdc instances in the integration\n") instancesContent, err := sfdc.GetInstancesAndChannels(instances) if err != nil { return err } if len(instancesContent) > 0 { if err = generateFolder(path.Join(folder, "sfdcinstances")); err != nil { return err } if err = generateFolder(path.Join(folder, "sfdcchannels")); err != nil { return err } for instance, channel := range instancesContent { instanceBytes, _ := apiclient.PrettifyJson([]byte(instance)) channelBytes, _ := apiclient.PrettifyJson([]byte(channel)) instanceName := getName([]byte(instance)) channelName := getName([]byte(channel)) clilog.Info.Printf("Storing sfdcinstance %s\n", instanceName) if err = apiclient.WriteByteArrayToFile( path.Join(folder, "sfdcinstances", instanceName+jsonExt), false, instanceBytes); err != nil { return err } clilog.Info.Printf("Storing sfdcchannel %s\n", channelName) if err = apiclient.WriteByteArrayToFile( path.Join(folder, "sfdcchannels", instanceName+fileSplitter+channelName+jsonExt), false, channelBytes); err != nil { return err } } } } if cloudBuild { clilog.Info.Printf("Storing cloudbuild.yaml\n") if err = apiclient.WriteByteArrayToFile( path.Join(baseFolder, "cloudbuild.yaml"), false, []byte(utils.GetCloudBuildYaml())); err != nil { return err } } if cloudDeploy { clilog.Info.Printf("Storing clouddeploy.yaml and skaffold.yaml\n") if err = apiclient.WriteByteArrayToFile( path.Join(baseFolder, "clouddeploy.yaml"), false, []byte(utils.GetCloudDeployYaml(name, env))); err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(baseFolder, "skaffold.yaml"), false, []byte(utils.GetSkaffoldYaml(name))); err != nil { return err } } if githubAction { clilog.Info.Printf("Storing Github Action\n") if err = apiclient.WriteByteArrayToFile( path.Join(baseFolder, name+".yaml"), false, []byte(utils.GetGithubAction(env, name))); err != nil { return err } } return err }, Example: `Generate scaffold for dev env using snapshot: ` + GetExample(5) + ` Generate scaffold for integration, but skip connectors: ` + GetExample(6) + ` Generate scaffold for integration and produce cloud build config: ` + GetExample(7) + ` Generate scaffold for integration and produce cloud deploy config: ` + GetExample(8) + `\n See samples/scaffold-sample for more details`, } var ( cloudBuild, cloudDeploy, skipConnectors, skipAuthconfigs, useUnderscore, extractCode bool env string ) const jsonExt = ".json" const emptyTestConfig = `{ "inputParameters": {} }` func init() { var name, userLabel, snapshot, version string var latest, githubAction bool ScaffoldCmd.Flags().StringVarP(&name, "name", "n", "", "Integration flow name") ScaffoldCmd.Flags().StringVarP(&version, "ver", "v", "", "Integration flow version") ScaffoldCmd.Flags().StringVarP(&userLabel, "user-label", "u", "", "Integration flow user label") ScaffoldCmd.Flags().StringVarP(&snapshot, "snapshot", "s", "", "Integration flow snapshot number") ScaffoldCmd.Flags().BoolVarP(&cloudBuild, "cloud-build", "", false, "Generate cloud build file; default is false") ScaffoldCmd.Flags().BoolVarP(&cloudDeploy, "cloud-deploy", "", false, "Generate cloud deploy files; default is false") ScaffoldCmd.Flags().BoolVarP(&githubAction, "github-action", "", false, "Generate Github Action to apply integration; default is false") ScaffoldCmd.Flags().StringVarP(&folder, "folder", "f", "", "Folder to generate the scaffolding") ScaffoldCmd.Flags().StringVarP(&env, "env", "e", "", "Environment name for the scaffolding") ScaffoldCmd.Flags().BoolVarP(&skipConnectors, "skip-connectors", "", false, "Exclude connectors from scaffold") ScaffoldCmd.Flags().BoolVarP(&skipAuthconfigs, "skip-authconfigs", "", false, "Exclude authconfigs from scaffold") ScaffoldCmd.Flags().BoolVarP(&useUnderscore, "use-underscore", "", false, "Use underscore as a file splitter; default is __") ScaffoldCmd.Flags().BoolVarP(&extractCode, "extract-code", "x", false, "Extract JavaScript and Jsonnet code as separate files; default is false") ScaffoldCmd.Flags().BoolVarP(&latest, "latest", "", true, "Scaffolds the version with the highest snapshot number in SNAPSHOT state. If none found, selects the highest snapshot in DRAFT state; default is true") _ = ScaffoldCmd.MarkFlagRequired("name") } func generateFolder(name string) (err error) { if _, err = os.Stat(name); !os.IsNotExist(err) { return nil } err = os.Mkdir(name, os.ModePerm) return err } func getName(authConfigResp []byte) string { var m map[string]string _ = json.Unmarshal(authConfigResp, &m) return m["displayName"] } func generateTestcases(testcases []byte, integrationBody []byte, folder string) error { var data []map[string]interface{} var testNames []string err := json.Unmarshal(testcases, &data) if err != nil { return fmt.Errorf("Error decoding JSON: %s", err) } for _, t := range data { jsonData, err := json.Marshal(t) if err != nil { return fmt.Errorf("Error encoding JSON: %s", err) } name, err := getTestCaseName(t) if err != nil { return fmt.Errorf("unable to get name: %v", err) } //check for duplicates if !slices.Contains(testNames, name) { testNames = append(testNames, name) } else { clilog.Warning.Println("two or more test cases have the same display name. only the most recent one will be used") } jsonData, err = apiclient.PrettifyJson(jsonData) if err != nil { return err } if err = apiclient.WriteByteArrayToFile( path.Join(folder, "tests", name+jsonExt), false, jsonData); err != nil { return err } testConfig, _ := integrations.GetInputParameters(integrationBody) if err = apiclient.WriteByteArrayToFile( path.Join(folder, "test-configs", name+jsonExt), false, testConfig); err != nil { return err } } return nil } func getTestCaseName(jsonData map[string]interface{}) (string, error) { if name, ok := jsonData["displayName"].(string); ok && name != "" { return removeNonAlphanumeric(name), nil } return "", fmt.Errorf("name not found") } func removeNonAlphanumeric(str string) string { reg, _ := regexp.Compile("[^a-zA-Z0-9-_]+") return reg.ReplaceAllString(str, "") }