cli/launchpad/root.go (110 lines of code) (raw):
package launchpad
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
// NewGenerate takes file patterns as input YAMLs and output Infrastructure as
// Code ready scripts based on specified output flavor.
//
// NewGenerate can be triggered by
//
// $ cft launchpad generate *.yaml
// $ cft lp g *.yaml
func NewGenerate(rawPaths []string, outFlavor OutputFlavor, outputDir string) {
// attempt to load all configs with best effort
log.Println("debug: output location", outputDir) // Remove after generate code is written
log.Println("debug: output flavor", outFlavor) // Remove after generate code is written
resources := loadResources(rawPaths)
log.Println(len(resources), "YAML documents loaded")
assembled := assembleResourcesToOrg(resources)
print(assembled.String()) // Place-holder for future trigger point of code generation
}
// OutputFlavor defines launchpad's generated output language.
type OutputFlavor int
const (
DeploymentManager OutputFlavor = iota
Terraform
)
// String returns the string representation of an OutputFlavor.
func (f OutputFlavor) String() string {
return []string{"DeploymentManager", "Terraform"}[f]
}
// newOutputFlavor parses string formatted output flavor and convert to internal format.
//
// Unsupported format given will terminate the application.
func NewOutputFlavor(fStr string) OutputFlavor {
switch strings.ToLower(fStr) {
case "deploymentmanager", "dm":
log.Println("Warning: Deployment Manager format not yet supported")
return DeploymentManager
case "terraform", "tf":
return Terraform
default:
log.Fatalln("Unsupported output flavor", fStr)
}
return -1
}
const yamlDelimiter = "---\n"
// loadResources attempts to load YAMLs from all given path patterns in best effort.
//
// loadResources will silently ignore file I/O related errors, attempt to parse all
// files and extract resources if possible.
func loadResources(rawPaths []string) []resourceHandler {
var buff []resourceHandler
for _, pathPattern := range rawPaths {
matches, err := filepath.Glob(pathPattern)
if err != nil {
log.Println("warning: Invalid file path pattern", pathPattern)
continue
}
for _, fp := range matches {
content, err := loadFile(fp)
if err != nil {
log.Println("warning: Unable to load requested file", fp)
continue
}
// Multiple YAML doc can exist within one file
docStrs := strings.Split(content, yamlDelimiter)
for _, docStr := range docStrs {
resource, err := loadYAML([]byte(docStr))
if err != nil {
log.Printf("warning: unable to parse YAML [%s]:\n%s", err.Error(), docStr)
continue
}
if err = resource.validate(); err != nil {
log.Printf("warning: YAML validation failed [%s]:\n%s", err.Error(), docStr)
continue
}
buff = append(buff, resource)
}
}
}
return buff
}
// loadFile return the file content with the specified relative path to current location.
//
// loadFile will attempt to load from filesystem directly first, a not found will attempt
// to load from statics variable generated from `$ go generate`. A user using output binary
// can in theory place their own file in matching relative path and overwrite the binary
// default.
func loadFile(fp string) (string, error) {
if content, err := os.ReadFile(fp); err == nil {
return string(content), nil
} else {
if !os.IsNotExist(err) {
fmt.Printf("Request file %s exists but cannot be read\n", fp)
return "", err
}
if content, ok := statics[fp]; ok { // attempt to load from binary statics
return content, nil
}
fmt.Printf("Requested file does not exist in filesystem nor generated binary %s\n", fp)
return "", os.ErrNotExist
}
}
// loadYAML loads given byte slice as a CFT resource.
//
// loadYAML takes two pass to load YAML, first to determine the CRD kind,
// second to load the YAML into specific CFT resource.
func loadYAML(docStr []byte) (resourceHandler, error) {
h := &headerYAML{}
err := yaml.Unmarshal(docStr, h)
if err != nil {
log.Printf("Malformed YAML")
return nil, err
}
kinds, ok := supportedVersion[h.APIVersion]
if !ok {
log.Printf("Not supported version")
return nil, errors.New("unsupported version")
}
resourceFunc, ok := kinds[h.kind()]
if !ok {
log.Printf("Not supported kind")
return nil, errors.New("unsupported custom resource kind for the version")
}
resource := resourceFunc()
err = yaml.Unmarshal(docStr, resource)
if err != nil {
log.Printf("Malformed YAML")
return nil, errors.New("unsupported custom resource kind")
}
return resource, nil
}