config/stack.go (186 lines of code) (raw):

// Copyright 2023 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 config import ( "fmt" "io/ioutil" "os" "path/filepath" "sort" "strings" ) // Stack represents the input config and output settings for this DeployStack type Stack struct { Settings Settings Config Config } // NewStack returns an initialized Stack func NewStack() Stack { s := Stack{} s.Settings = Settings{} return s } func (s *Stack) findAndReadConfig(path string) (Config, error) { config := Config{} candidates := []string{ ".deploystack/deploystack.yaml", ".deploystack/deploystack.json", "deploystack.json", } configPath := "" for _, v := range candidates { candidate := filepath.Join(path, v) if _, err := os.Stat(candidate); err == nil { configPath = candidate break } } if configPath == "" { return config, ErrConfigNotExist } content, err := ioutil.ReadFile(configPath) if err != nil { return config, fmt.Errorf("unable to find or read config (%s) file: %s", configPath, err) } switch filepath.Ext(configPath) { case ".yaml": config, err = NewConfigYAML(content) if err != nil { return config, fmt.Errorf("unable to parse config file: %s", err) } return config, nil default: config, err = NewConfigJSON(content) if err != nil { return config, fmt.Errorf("unable to parse config file: %s", err) } } return config, nil } // ErrConfigNotExist is what happens when a config file either does not exist // or exists but is not readable. var ErrConfigNotExist = fmt.Errorf("could not find and parse a config file") func (s *Stack) findDSFolder(path, folder string) (string, error) { switch folder { case "messages": if s.Config.PathMessages != "" { return s.Config.PathMessages, nil } case "scripts": if s.Config.PathScripts != "" { return s.Config.PathScripts, nil } } dsPath := filepath.Join(path, folder) if _, err := os.Stat(dsPath); err == nil { return dsPath, nil } dsPath = filepath.Join(path, ".deploystack", folder) if _, err := os.Stat(dsPath); err == nil { return dsPath, nil } return fmt.Sprintf("./%s", folder), fmt.Errorf("requirement (%s) was not found either in the root, or in .deploystack folder nor was it set in deploystack.json", folder) } func (s *Stack) findTFFolder(path string) (string, error) { if s.Config.PathTerraform != "" { return s.Config.PathTerraform, nil } mains := []string{} err := filepath.Walk(path, func(walkpath string, info os.FileInfo, err error) error { if info == nil { return fmt.Errorf("info is nil: walkpath: %s err: %s", walkpath, err) } if info.Name() == "main.tf" { dir := filepath.Dir(walkpath) mains = append(mains, dir) return err } return nil }) if err != nil { return "", fmt.Errorf("findTFFolder: could not find a terraform folder:, %s", err) } // I want the top most main file here. And that should be the shortest sort.Slice(mains, func(i, j int) bool { return len(mains[i]) < len(mains[j]) }) if len(mains) > 0 { return filepath.Rel(path, mains[0]) } return "", nil } // FindAndRead figures out a default config, or reads it if it is there // has option to insure various things and folders exist func (s *Stack) FindAndRead(path string, required bool) error { errs := []error{} config, err := s.findAndReadConfig(path) s.Config = config errs = append(errs, err) tfPath, err := s.findTFFolder(path) s.Config.PathTerraform = tfPath errs = append(errs, err) scriptPath, err := s.findDSFolder(path, "scripts") s.Config.PathScripts, _ = filepath.Rel(path, scriptPath) errs = append(errs, err) messagePath, err := s.findDSFolder(path, "messages") s.Config.PathMessages, _ = filepath.Rel(path, messagePath) errs = append(errs, err) if config.Description == "" { descText := fmt.Sprintf("%s/description.txt", messagePath) description, err := ioutil.ReadFile(descText) s.Config.Description = string(description) errs = append(errs, err) } s.Config.convertHardset() s.Config.defaultAuthorSettings() if required && len(errs) > 0 { return errs[0] } return nil } // FindAndReadRequired finds and reads in a Config from a json file. func (s *Stack) FindAndReadRequired(path string) error { return s.FindAndRead(path, true) } // AddSetting stores a setting key/value pair. func (s *Stack) AddSetting(key, value string) { s.Settings.Add(key, value) } // AddSettingComplete passes a completely intact setting to the underlying // setting structure func (s *Stack) AddSettingComplete(set Setting) { s.Settings.AddComplete(set) } // GetSetting returns a setting value. func (s *Stack) GetSetting(key string) string { set := s.Settings.Find(key) if set != nil { return set.Value } return "" } // DeleteSetting removes a setting value. func (s *Stack) DeleteSetting(key string) { for i, v := range s.Settings { if v.Name == key { s.Settings = append(s.Settings[:i], s.Settings[i+1:]...) } } } // Terraform returns all of the settings as a Terraform variables format. func (s Stack) Terraform() string { result := strings.Builder{} s.Settings.Sort() for _, v := range s.Settings { if v.Name == "" { continue } label := v.TFvarsName() if label == "project_name" { continue } if label == "stack_name" { continue } if len(v.Value) == 0 && len(v.List) == 0 && v.Map == nil { continue } result.WriteString(v.TFVars()) } return result.String() } // TerraformFile exports TFVars format to input file. func (s Stack) TerraformFile(filename string) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() if _, err = f.WriteString(s.Terraform()); err != nil { return err } return nil }