blob: 64a1b3fa9d3b1f09fccb0d677f5cd6f6bf720473 [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This file contains a program that reads cobalt configuration in a YAML format
// and outputs it as a CobaltConfig serialized protocol buffer.
package main
import (
"config"
"config_parser"
"config_validator"
"flag"
"fmt"
"github.com/golang/glog"
"io"
"io/ioutil"
"os"
"strings"
"time"
)
var (
repoUrl = flag.String("repo_url", "", "URL of the repository containing the config. Exactly one of 'repo_url', 'config_file' or 'config_dir' must be specified.")
configDir = flag.String("config_dir", "", "Directory containing the config. Exactly one of 'repo_url', 'config_file' or 'config_dir' must be specified.")
configFile = flag.String("config_file", "", "File containing the config for a single project. Exactly one of 'repo_url', 'config_file' or 'config_dir' must be specified.")
outFile = flag.String("output_file", "", "File to which the serialized config should be written. Defaults to stdout.")
checkOnly = flag.Bool("check_only", false, "Only check that the configuration is valid.")
skipValidation = flag.Bool("skip_validation", false, "Skip validating the config, write it no matter what.")
gitTimeoutSec = flag.Int64("git_timeout", 60, "How many seconds should I wait on git commands?")
customerId = flag.Int64("customer_id", -1, "Customer Id for the config to be read. Must be set if and only if 'config_file' is set.")
projectId = flag.Int64("project_id", -1, "Project Id for the config to be read. Must be set if and only if 'config_file' is set.")
outFormat = flag.String("out_format", "bin", "Specifies the output format. Supports 'bin' (serialized proto), 'b64' (serialized proto to base 64) and 'cpp' (ta C++ file containing a variable with a base64-encoded serialized proto.)")
varName = flag.String("var_name", "config", "When using the 'cpp' output format, this will specify the variable name to be used in the output.")
namespace = flag.String("namespace", "", "When using the 'cpp' output format, this will specify the comma-separated namespace within which the config variable must be places.")
listConfigFiles = flag.Bool("list_config_files", false, "List the files that comprise the configuration of Cobalt. This should be used in conjunction with the 'config_dir' flag only.")
)
func main() {
flag.Parse()
if (*repoUrl == "") == (*configDir == "") == (*configFile == "") {
glog.Exit("Exactly one of 'repo_url', 'config_file' and 'config_dir' must be set.")
}
if *configFile == "" && (*customerId >= 0 || *projectId >= 0) {
glog.Exit("'customer_id' and 'project_id' must be set if and only if 'config_file' is set.")
}
if *configFile != "" && (*customerId < 0 || *projectId < 0) {
glog.Exit("If 'config_file' is set, both 'customer_id' and 'project_id' must be set.")
}
if *outFile != "" && *checkOnly {
glog.Exit("'output_file' does not make sense if 'check_only' is set.")
}
if *listConfigFiles && *configDir == "" {
glog.Exit("'list_config_files' is only compatible with 'config_dir' being set.")
}
var configLocation string
if *repoUrl != "" {
configLocation = *repoUrl
} else if *configFile != "" {
configLocation = *configFile
} else {
configLocation = *configDir
}
if *listConfigFiles {
files, err := config_parser.GetConfigFilesListFromConfigDir(configLocation)
if err != nil {
glog.Exit(err)
}
fmt.Println(strings.Join(files, "\n"))
os.Exit(0)
}
var outputFormatter config_parser.OutputFormatter
switch *outFormat {
case "bin":
outputFormatter = config_parser.BinaryOutput
case "b64":
outputFormatter = config_parser.Base64Output
case "cpp":
namespaceList := []string{}
if *namespace != "" {
namespaceList = strings.Split(*namespace, ",")
}
outputFormatter = config_parser.CppOutputFactory(*varName, namespaceList, configLocation)
default:
glog.Exitf("'%v' is an invalid out_format parameter. 'bin', 'b64' and 'cpp' are the only valid values for out_format.", *outFormat)
}
// First, we parse the configuration from the specified location.
var c config.CobaltConfig
var err error
if *repoUrl != "" {
gitTimeout := time.Duration(*gitTimeoutSec) * time.Second
c, err = config_parser.ReadConfigFromRepo(*repoUrl, gitTimeout)
} else if *configFile != "" {
c, err = config_parser.ReadConfigFromYaml(*configFile, uint32(*customerId), uint32(*projectId))
} else {
c, err = config_parser.ReadConfigFromDir(*configDir)
}
if err != nil {
glog.Exit(err)
}
if !*skipValidation {
if err = config_validator.ValidateConfig(&c); err != nil {
glog.Exit(err)
}
}
// Then, we serialize the configuration.
configBytes, err := outputFormatter(&c)
if err != nil {
glog.Exit(err)
}
// Check that the output file is not empty.
if len(configBytes) == 0 {
glog.Exit("Output file is empty.")
}
// If no errors have occured yet and checkOnly was set, we are done.
if *checkOnly {
fmt.Printf("%s OK\n", configLocation)
os.Exit(0)
}
// By default we print the output to stdout.
w := os.Stdout
// If an output file is specified, we write to a temporary file and then rename
// the temporary file with the specified output file name.
if *outFile != "" {
if w, err = ioutil.TempFile("", "cobalt_config"); err != nil {
glog.Exit(err)
}
defer w.Close()
}
_, err = w.Write(configBytes)
if err != nil {
glog.Exit(err)
}
if *outFile != "" {
if err := os.Rename(w.Name(), *outFile); err != nil {
// Rename doesn't work if /tmp is in a different partition. Attempting to copy.
// TODO(azani): Look into doing this atomically.
in, err := os.Open(w.Name())
if err != nil {
glog.Exit(err)
}
defer in.Close()
out, err := os.Create(*outFile)
if err != nil {
glog.Exit(err)
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
glog.Exit(err)
}
}
}
os.Exit(0)
}