config/config.go (320 lines of code) (raw):
// Package config holds all of the data structures for DeployStack.
// Having them in main package caused circular dependecy issues.
package config
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/yaml.v2"
)
// Config represents the settings this app will collect from a user. It should
// be in a json file. The idea is minimal programming has to be done to setup
// a DeployStack and export out a tfvars file for terraform part of solution.
type Config struct {
Title string `json:"title" yaml:"title"`
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`
Duration int `json:"duration" yaml:"duration"`
Project bool `json:"collect_project" yaml:"collect_project"`
ProjectNumber bool `json:"collect_project_number" yaml:"collect_project_number"`
BillingAccount bool `json:"collect_billing_account" yaml:"collect_billing_account"`
Domain bool `json:"register_domain" yaml:"register_domain"`
Region bool `json:"collect_region" yaml:"collect_region"`
RegionType string `json:"region_type" yaml:"region_type"`
RegionDefault string `json:"region_default" yaml:"region_default"`
Zone bool `json:"collect_zone" yaml:"collect_zone"`
HardSet map[string]string `json:"hard_settings" yaml:"hard_settings"`
CustomSettings Customs `json:"custom_settings" yaml:"custom_settings"`
AuthorSettings Settings `json:"author_settings" yaml:"author_settings"`
ConfigureGCEInstance bool `json:"configure_gce_instance" yaml:"configure_gce_instance"`
DocumentationLink string `json:"documentation_link" yaml:"documentation_link"`
PathTerraform string `json:"path_terraform" yaml:"path_terraform"`
PathMessages string `json:"path_messages" yaml:"path_messages"`
PathScripts string `json:"path_scripts" yaml:"path_scripts"`
Projects Projects `json:"projects" yaml:"projects"`
Products []Product `json:"products" yaml:"products"`
WD string `json:"-" yaml:"-"`
}
func (c *Config) convertHardset() {
for i, v := range c.HardSet {
c.AuthorSettings.AddComplete(Setting{Name: i, Value: v, Type: "string"})
}
// Blow hardset away so that if anywhere is looking for them, it fails.
c.HardSet = nil
}
// wd used to be unexported, but not it is not. Left the getter and setter to
// not break anything
// Getwd gets the working directory for the config.
func (c *Config) Getwd() string {
return c.WD
}
// Setwd sets the working directory for the config.
func (c *Config) Setwd(wd string) {
c.WD = wd
}
// Copy produces a copy of a config file for manipulating it without changing
// the original
func (c Config) Copy() Config {
out := Config{}
out.WD = c.WD
out.Name = c.Name
out.Title = c.Title
out.Project = c.Project
out.ProjectNumber = c.ProjectNumber
out.Region = c.Region
out.RegionType = c.RegionType
out.RegionDefault = c.RegionDefault
out.Zone = c.Zone
out.Description = c.Description
out.Duration = c.Duration
out.DocumentationLink = c.DocumentationLink
out.Domain = c.Domain
out.ConfigureGCEInstance = c.ConfigureGCEInstance
out.PathTerraform = c.PathTerraform
out.PathMessages = c.PathMessages
out.PathScripts = c.PathScripts
for _, v := range c.AuthorSettings {
out.AuthorSettings.AddComplete(v)
}
for _, v := range c.CustomSettings {
out.CustomSettings = append(out.CustomSettings, v)
}
for _, v := range c.Products {
out.Products = append(out.Products, v)
}
return out
}
// Marshal returns a string representation in format `json` or `yaml`
func (c Config) Marshal(format string) ([]byte, error) {
if format == "yaml" {
out, err := yaml.Marshal(&c)
if err != nil {
return nil, fmt.Errorf("cannot export test: %s", err)
}
return out, nil
}
out, err := json.MarshalIndent(&c, "", "\t")
if err != nil {
return nil, fmt.Errorf("cannot export test: %s", err)
}
return out, nil
}
func (c *Config) defaultAuthorSettings() {
for i, v := range c.AuthorSettings {
if v.Type == "" {
v.Type = "string"
c.AuthorSettings[i] = v
}
}
}
// GetAuthorSettings delivers the combined Hardset and AuthorSettings variables
func (c *Config) GetAuthorSettings() Settings {
c.convertHardset()
c.AuthorSettings.Sort()
return c.AuthorSettings
}
// ComputeName uses the git repo in the working directory to compute the
// shortname for the application.
func (c *Config) ComputeName(path string) error {
repo, err := git.PlainOpen(path)
if err != nil {
return fmt.Errorf("could not open local git directory: %s", err)
}
remotes, err := repo.Remotes()
if err != nil {
return err
}
remote := ""
for _, v := range remotes {
for _, url := range v.Config().URLs {
if strings.Contains(strings.ToLower(url), "googlecloudplatform") {
remote = strings.ToLower(url)
}
}
}
// Fixes bug where ssh called repos have issues. Super edge case, but
// now its all testable
remote = strings.ReplaceAll(remote, "git@github.com:", "https://github.com/")
u, err := url.Parse(remote)
if err != nil {
return fmt.Errorf("could not parse git url: %s", err)
}
shortname := filepath.Base(u.Path)
shortname = strings.ReplaceAll(shortname, ".git", "")
shortname = strings.ReplaceAll(shortname, "deploystack-", "")
c.Name = shortname
return nil
}
// NewConfigJSON returns a Config object from a file read.
func NewConfigJSON(content []byte) (Config, error) {
result := Config{}
if err := json.Unmarshal(content, &result); err != nil {
return result, fmt.Errorf("unable to convert content to Config: %s", err)
}
return result, nil
}
// NewConfigYAML returns a Config object from a file read.
func NewConfigYAML(content []byte) (Config, error) {
result := Config{}
if err := yaml.Unmarshal(content, &result); err != nil {
return result, fmt.Errorf("unable to convert content to Config: %s", err)
}
return result, nil
}
// Product is some info about a GCP product
type Product struct {
Info string `json:"info" yaml:"info"`
Product string `json:"product" yaml:"product"`
}
// Project represets a GCP project for use in a stack
type Project struct {
Name string `json:"variable_name" yaml:"variable_name"`
UserPrompt string `json:"user_prompt" yaml:"user_prompt"`
SetAsDefault bool `json:"set_as_default" yaml:"set_as_default"`
Value string `json:"value" yaml:"value"`
}
// Projects is a list of projects that we will collect info for
type Projects struct {
Items []Project `json:"items" yaml:"items"`
AllowDuplicates bool `json:"allow_duplicates" yaml:"allow_duplicates"`
}
// Setting is a item that will be translated to a variable in a terraform file
type Setting struct {
Name string `json:"name" yaml:"name"`
Value string `json:"value" yaml:"value"`
Type string `json:"type" yaml:"type"`
List []string `json:"list" yaml:"list"`
Map map[string]string `json:"map" yaml:"map"`
}
// TFVars emits the name value combination here in away that terraform excepts
// in a tfvars file
func (s *Setting) TFVars() string {
return fmt.Sprintf("%s=%s\n", s.TFvarsName(), s.TFvarsValue())
}
// TFvarsName formats the name for the tfvars format
func (s Setting) TFvarsName() string {
name := strings.ToLower(strings.ReplaceAll(s.Name, " ", "_"))
return name
}
// TFvarsValue formats the value for the tfvars format
func (s Setting) TFvarsValue() string {
result := ""
// If we used the workaround for lists in strings, convert it to a list
// under the covers
if s.Value != "" && s.Value[0:1] == "[" {
replacer := strings.NewReplacer("[", "", "]", "")
s.List = strings.Split(replacer.Replace(s.Value), ",")
s.Type = "list"
s.Value = ""
}
switch s.Type {
case "string", "":
result = fmt.Sprintf("\"%s\"", s.Value)
case "list":
tmp := []string{}
for _, v := range s.List {
tmp = append(tmp, fmt.Sprintf("\"%s\"", v))
}
str := strings.Join(tmp, ",")
result = fmt.Sprintf("[%s]", str)
case "map":
tmp := []string{}
for i, v := range s.Map {
tmp = append(tmp, fmt.Sprintf("%s=\"%s\"", i, v))
}
sort.Strings(tmp)
str := strings.Join(tmp, ",")
result = fmt.Sprintf("{%s}", str)
default:
result = s.Value
}
return result
}
// Settings are a collection of setting
type Settings []Setting
// AddComplete adds an whole setting to the settings control
func (s *Settings) AddComplete(set Setting) {
setting := s.Find(set.Name)
if setting != nil {
s.Replace(set)
return
}
(*s) = append((*s), set)
return
}
// Add either creates a new setting or updates the existing one
func (s *Settings) Add(key, value string) {
k := strings.ToLower(key)
set := s.Find(key)
if set != nil {
set.Name = key
set.Value = value
set.Type = "string"
s.Replace(*set)
return
}
set = &Setting{Name: k, Value: value, Type: "string"}
(*s) = append((*s), *set)
return
}
// Sort sorts the slice according to Setting.Name ascendings
func (s *Settings) Sort() {
sort.Slice(*s, func(i, j int) bool {
return (*s)[i].Name < (*s)[j].Name
})
}
// Replace will look for a setting with the same name, and overwrite the value
func (s *Settings) Replace(set Setting) {
for i, v := range *s {
if v.Name == set.Name {
(*s)[i] = set
}
}
}
// Search returns all settings whose names contain a particular string
func (s *Settings) Search(q string) Settings {
result := Settings{}
for _, v := range *s {
if strings.Contains(v.Name, q) {
result = append(result, v)
}
}
return result
}
// Find locates a setting in the slice
func (s *Settings) Find(key string) *Setting {
k := strings.ToLower(key)
for _, v := range *s {
if v.Name == k {
return &v
}
}
return nil
}
// Custom represents a custom setting that we would like to collect from a user
// We will collect these settings from the user before continuing.
type Custom struct {
Setting `json:"-" yaml:"-"`
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`
Default string `json:"default" yaml:"default"`
Options []string `json:"options" yaml:"options"`
PrependProject bool `json:"prepend_project" yaml:"prepend_project"`
Validation string `json:"validation,omitempty" yaml:"validation,omitempty"`
Project string `json:"-" yaml:"-"`
}
// Customs are a slice of Custom variables.
type Customs []Custom
// Get returns one Custom Variable
func (cs Customs) Get(name string) Custom {
for _, v := range cs {
if v.Name == name {
return v
}
}
return Custom{}
}
// Report is collection of data about multiple configs in the same root
// used for multi stack repos
type Report struct {
Path string
WD string
Config Config
}
// NewReport Generates a new config report for a given file
func NewReport(file string) (Report, error) {
result := Report{Path: file}
result.WD = strings.ReplaceAll(filepath.Dir(file), "/.deploystack", "")
dat, err := os.ReadFile(file)
if err != nil {
return result, err
}
switch filepath.Ext(file) {
case ".json":
result.Config, err = NewConfigJSON(dat)
if err != nil {
return result, err
}
case ".yaml":
result.Config, err = NewConfigYAML(dat)
if err != nil {
return result, err
}
}
return result, nil
}
// FindConfigReports walks through a directory and finds all of the configs in
// the folder
func FindConfigReports(dir string) ([]Report, error) {
var result []Report
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if info.Name() == "deploystack.json" || info.Name() == "deploystack.yaml" {
cr, err := NewReport(path)
if err != nil {
return err
}
result = append(result, cr)
}
return nil
})
return result, err
}