func getConfig(file string) (*ConfigFile, error) { Logger.Debug("Checking for config file in location:", file) var unmarshal = func(jsonBlob []byte) (*ConfigFile, error) { configFile := NewConfigFile() if err := json.Unmarshal(jsonBlob, &configFile); err != nil { msg := "Can't find or having trouble reading " + file + ". Please create the file or address syntax issues. System error:" + err.Error() // return nil, ReadFileError.New(msg) return nil, errors.New(msg) } else { // Augment configfile with it's location configFile.Location = file configFile.LocationDir = file[:strings.LastIndex(file, "/")] return configFile, nil } } if jsonBlob, err := ioutil.ReadFile(file); err != nil { return nil, errors.New("Can't find " + file) } else { if jsonBlob == nil { msg := file + " not found." return nil, errors.New(msg) } else { return unmarshal(jsonBlob) } } }
func (api *DockerApi) GetImageDetails(fqImageName string) (*ApiDockerImage, error) { url := strings.Join( []string{ api.configFile.DockerEndpoint, "images", fqImageName, "json", }, "/", ) // For testing, also need to `import "encoding/json"` // result := map[string]json.RawMessage{} result := ApiDockerImage{} Logger.Trace("GetImageDetails Api call to:", url) if resp, err := napping.Get(url, nil, &result, nil); err != nil { Logger.Error("Error while getting Image information from docker,", err) return nil, err } else { switch resp.Status() { case 200: Logger.Trace(result) return &result, nil case 404: Logger.Debug("Image not found") return nil, nil } return nil, nil } }
// ListContainers that are running func (api *DockerApi) ListContainers(imageName string) ([]string, error) { endpoint := api.configFile.DockerEndpoint baseUrl := strings.Join( []string{ endpoint, "containers", "json", }, "/", ) params := strings.Join( []string{ "all=true", }, "&", ) url := baseUrl + "?" + params Logger.Trace("ListContainers Api call:", url) resp, _ := http.Get(url) defer resp.Body.Close() if body, err := ioutil.ReadAll(resp.Body); err != nil { Logger.Error("Could not contact docker endpoint:", endpoint) return nil, err } else { switch resp.StatusCode { case 200: var jsonResult []ApiContainer if err := json.Unmarshal(body, &jsonResult); err != nil { Logger.Error("Error while trying to marshall result, body:", jsonResult, " - Error:", err) return nil, err } else { var ids []string for i := range jsonResult { if jsonResult[i].Image == imageName { ids = append(ids, jsonResult[i].Id) } } Logger.Debug("Containers found for", imageName, ids) return ids, nil } case 404: Logger.Warn("Bad param supplied to API") case 500: Logger.Error("Error while trying to communicate to docker endpoint:", endpoint) } } return nil, nil }
// tries to find config file in user home, then if it cannot find one there // will try to find a config file in the local running directory func findConfig(dir string) (*ConfigFile, error) { configFileName := Constants().configFileName configFile := NewConfigFile() logConfigFile := func(configFile *ConfigFile) { Logger.Debug("Found config file.") Logger.Trace("Config file contents:", *configFile) } // try dir specified localConfigPath := FilePath(dir, configFileName) configFile, err := getConfig(localConfigPath) if err != nil { msg := "Config: " + localConfigPath + " may not exist or cannot be read. " + err.Error() Logger.Debug(msg) } else { logConfigFile(configFile) return configFile, nil } // try running directory next pwd, err := os.Getwd() if err != nil { return nil, err } Logger.Debug("Checking for config file in local pwd: " + pwd + "/" + configFileName) pwdConfigPath := FilePath(pwd, ".test-flight", configFileName) configFile, err = getConfig(pwdConfigPath) if err != nil { msg := "Config: " + localConfigPath + " may not exist or cannot be read. " + err.Error() Logger.Debug(msg) } else { logConfigFile(configFile) return configFile, nil } // try home usr, err := user.Current() // to get user home, get user first if err != nil { Logger.Error("Can't read user home.") // return nil, ReadFileError.New("Can't read user home.") return nil, errors.New("Can't read user home.") } homeConfigPath := FilePath(usr.HomeDir, ".test-flight", configFileName) Logger.Debug("Checking for config file in user HOME: ", homeConfigPath) configFile, err = getConfig(homeConfigPath) if err != nil { msg := "Config: " + localConfigPath + " may not exist or cannot be read. " + err.Error() Logger.Debug(msg) } else { logConfigFile(configFile) return configFile, nil } return nil, errors.New("Cannot find config file, Please supply the config file.") }
// listens for ctrl-c from the user func CaptureUserCancel(containerChannel *ContainerChannel, wg *sync.WaitGroup) { syschan := make(chan os.Signal, 1) signal.Notify(syschan, os.Interrupt) signal.Notify(syschan, syscall.SIGTERM) go func() { <-syschan Logger.Debug("Canceling user operation...") if wg != nil { *containerChannel <- "canceled" wg.Done() } defer close(*containerChannel) return }() }
func NewDockerApi(meta *ApplicationMeta, configFile *ConfigFile, buildFile *BuildFile) *DockerApi { api := DockerApi{ meta: meta, configFile: configFile, buildFile: buildFile, } client, err := docker.NewClient(configFile.DockerEndpoint) if err != nil { Logger.Error("Docker API Client Error:", err) os.Exit(ExitCodes["docker_error"]) } Logger.Debug("Docker client created using endpoint:", configFile.DockerEndpoint) api.client = client return &api }
func (fc *FlightControls) CheckConfigs(app *TestFlight, options *CommandOptions) (*ConfigFile, *BuildFile, error) { // Setup Logging // TODO: Replace with only Load once app.Init() is gone Logger.Load(len(options.Verbose)) Logger.Setup() // Set vars if options.Dir == "" { options.Dir = "./" // set to local working dir as default } Logger.Info("Using directory:", options.Dir) // Prereqs app.SetDir(options.Dir) configFile, err := ReadConfigFile(options.Configfile, options.Dir) if err != nil { return nil, nil, err } app.SetConfigFile(configFile) Logger.Info("Using configfile:", configFile.Location) // Get the buildfile // TODO: as more Control funcs get created refactor this below buildFile, err := fc.CheckBuild(options.Dir) if err != nil { return nil, nil, err } app.SetBuildFile(buildFile) Logger.Info("Using buildfile:", buildFile.Location) Logger.Debug("buildfile - contents:", *buildFile) if err := fc.CheckRequiredFiles(options.Dir, options.SingleFileMode, buildFile); err != nil { return nil, nil, err } return configFile, buildFile, nil }
// http get using golang packaged lib func (api *DockerApi) ShowImageGo() (*ApiDockerImage, error) { endpoint := api.configFile.DockerEndpoint url := strings.Join( []string{ endpoint, "images", api.buildFile.ImageName + ":" + api.buildFile.Tag, "json", }, "/", ) resp, _ := http.Get(url) defer resp.Body.Close() if body, err := ioutil.ReadAll(resp.Body); err != nil { Logger.Error("Could not contact docker endpoint:", endpoint) return nil, err } else { switch resp.StatusCode { case 200: var jsonResult ApiDockerImage if err := json.Unmarshal(body, &jsonResult); err != nil { Logger.Error(err) return nil, err } return &jsonResult, nil case 404: Logger.Debug("Image not found") case 500: Logger.Error("Error while trying to communicate to docker endpoint:", endpoint) } return nil, nil } }
func TarDirectory(tw *tar.Writer, dir string, ignoreList []string) error { Logger.Trace("Taring: ", dir) // Add Dockerfile to the ignore list // This can also help prevent existing Dockerfiles from being used // and accidently shipped in the context. ignoreList = append(ignoreList, "Dockerfile") shouldIgnore := func(file string) bool { for _, entry := range ignoreList { if entry == file { return true } } return false } var archive = func(files []os.FileInfo) error { Logger.Trace("Found files to archive into context: ", len(files)) for _, file := range files { if shouldIgnore(file.Name()) { Logger.Debug("Ignoring:", file.Name()) continue } fullFilePath := strings.Join([]string{dir, file.Name()}, "/") if file.IsDir() { TarDirectory(tw, fullFilePath, ignoreList) continue } hdr := &tar.Header{ Name: fullFilePath, Size: file.Size(), } if err := tw.WriteHeader(hdr); err != nil { Logger.Error("Could not write context archive header", err) return err } if bytes, err := ioutil.ReadFile(fullFilePath); err != nil { Logger.Error("Could not read context file: ["+fullFilePath+"]", err) return err } else { if _, err := tw.Write(bytes); err != nil { Logger.Error("Could not archive context file: ["+fullFilePath+"]", err) return err } } Logger.Trace("Archived into context the file: [" + fullFilePath + "]") } Logger.Trace("Successfully archived context", dir) return nil } if filesFromDisk, err := ioutil.ReadDir(dir); err != nil { Logger.Error("Error while trying to tar ["+dir+"]", err) return err } else { return archive(filesFromDisk) } }
func (cmd *LaunchCommand) Execute(args []string) error { Logger.Info("Launching Tests... in dir:", cmd.Options.Dir) Logger.Debug("Force:", cmd.Options.Force) var wg sync.WaitGroup // used for channels // Check Config and Buildfiles configFile, buildFile, err := cmd.Controls.CheckConfigs(cmd.App, cmd.Options) if err != nil { return err } dc := NewDockerApi(cmd.App.AppState.Meta, configFile, buildFile) dc.ShowInfo() if err := dc.createTestTemplates(*cmd.Options); err != nil { return err } Logger.Trace("Created test-flight templates.") // Register channel so we can watch for events as they happen eventsChannel := make(ApiChannel) go watchForEventsOn(eventsChannel) dc.RegisterChannel(eventsChannel) fqImageName := cmd.App.AppState.BuildFile.ImageName + ":" + buildFile.Tag getImageId := func() (string, error) { if image, err := dc.GetImageDetails(fqImageName); err != nil { return "", err } else { // response from endpoint (but could be 404) if image == nil { // doesn't exist if newImage, err := dc.CreateDockerImage(fqImageName, cmd.Options); err != nil { return "", err } else { return newImage, nil } } else { // exists if cmd.Options.Force { // recreate anyway if newImage, err := dc.CreateDockerImage(fqImageName, cmd.Options); err != nil { return "", err } else { return newImage, nil } } else { // warn user Logger.Warn("Will be starting a container from an image which already exists.") return image.Id, nil } } return image.Id, nil } } createAndStartContainerFrom := func(imageId string) error { if container, err := dc.CreateContainer(imageId); err != nil { return err } else { Logger.Trace("Docker Container to start:", container.Id) if _, err := dc.StartContainer(container.Id); err != nil { return err } else { wg.Add(1) containerChannel := dc.Attach(container.Id) go watchContainerOn(containerChannel, &wg) wg.Wait() return nil } } } if imageId, err := getImageId(); err != nil { return err } else { msg := "Launching docker container: " + imageId Logger.Console(msg) return createAndStartContainerFrom(imageId) } return nil }
// CreateDockerImage creates a docker image by first creating the Dockerfile // in memory and then tars it, prior to sending it along to the docker // endpoint. // Note: once the image creation starts the only way to know if it succeeded // is to query for it again. func (api *DockerApi) CreateDockerImage(fqImageName string, options *CommandOptions) (string, error) { Logger.Debug("Creating Docker image by attempting to build Dockerfile: " + fqImageName) tmplName := "Dockerfile" // file ext of `.tmpl` is implicit, see below var requiredDockerFile = RequiredFile{ Name: "Test-Flight Dockerfile", FileName: tmplName, FileType: "f", } modeDir := func() string { if options.SingleFileMode { return "filemode" } else { return "dirmode" } }() dockerfileBuffer := bytes.NewBuffer(nil) tarbuf := bytes.NewBuffer(nil) // Dockerfile mem dockerfile := bufio.NewWriter(dockerfileBuffer) // Or create file templateOutputDir := getTemplateDir(api.configFile) Logger.Trace(templateOutputDir, requiredDockerFile.FileName) fileToCreate := FilePath(templateOutputDir.FileName, requiredDockerFile.FileName) Logger.Debug("Trying to build:", fileToCreate) dockerfileOut, err := os.Create(fileToCreate) if err != nil { Logger.Error("Could not write Dockerfile", err) return "", err } defer dockerfileOut.Close() // The directory where the templates used to create inventory and playbook templates := func() string { // could have been simpler but I want to log path at this level var templatePath string if api.configFile.UseSystemDockerTemplates { templatePath = FilePath(api.configFile.TestFlightAssets, "templates", "system") } else { templatePath = FilePath(api.configFile.TestFlightAssets, "templates", "user") } Logger.Debug("Using template dir:", templatePath) return templatePath }() // This is used below in `ExecuteTemplate()` templateInputDir := FilePath(templates, modeDir) pattern := filepath.Join(templateInputDir, requiredDockerFile.FileName+"*.tmpl") tmpl := template.Must(template.ParseGlob(pattern)) // In Mem if err := tmpl.ExecuteTemplate(dockerfile, requiredDockerFile.FileName, *api.getTemplateVar()); err != nil { Logger.Error("template execution: %s", err) return "", err } // File if err := tmpl.ExecuteTemplate(dockerfileOut, requiredDockerFile.FileName, *api.getTemplateVar()); err != nil { Logger.Error("template execution: %s", err) return "", err } // For in-mem flush buffer here dockerfile.Flush() // Logger.Trace("Dockerfile buffer len", dockerfileBuffer.Len()) Logger.Trace("Dockerfile:", dockerfileBuffer.String()) // Logger.Info("Created Dockerfile: " + fqImageName) currTime := time.Now() // Add Dockerfile to archive, break out tr := tar.NewWriter(tarbuf) tr.WriteHeader(&tar.Header{ Name: "Dockerfile", Size: int64(dockerfileBuffer.Len()), ModTime: currTime, AccessTime: currTime, ChangeTime: currTime, }) tr.Write(dockerfileBuffer.Bytes()) // Add Context to archive // tar test directory TarDirectory(tr, api.meta.Dir, api.buildFile.Ignore) tr.Close() var wg sync.WaitGroup // used for channels wg.Add(1) Logger.Trace("Building image...") buildChannel := make(ContainerChannel) // go watchContainerOn(buildChannel, &wg) api.BuildImage(tarbuf, fqImageName, &buildChannel, &wg) // go CaptureUserCancel(&buildChannel) select { case value := <-buildChannel: switch value { case "ok": failMsg := "Docker Image [" + fqImageName + "] failed to build." // We got an ok but don't know if it is legit. Possible to get no error // but image will be nil (successful api call but image doesn't exist) if image, err := api.GetImageDetails(fqImageName); err != nil || image == nil { return "", errors.New(failMsg) } else { Logger.Info("Successfully built Docker image: " + fqImageName) return image.Id, nil } case "canceled": msg := "User Canceled docker creation." Logger.Warn(msg, "User must manually stop last running container.") return fqImageName, errors.New(msg) } } wg.Wait() Logger.Info("Successfully built Docker image: " + fqImageName) return fqImageName, nil }
// Returns container name deleted, empty string if none, and an // error (if any) func (api *DockerApi) DeleteContainer(name string) ([]DeletedContainer, error) { var deletedContainers []DeletedContainer var returnError error endpoint := api.configFile.DockerEndpoint delete := func(id string) (*string, error) { baseUrl := strings.Join( []string{ endpoint, "containers", id, }, "/", ) params := strings.Join( []string{ "v=true", "force=true", }, "&", ) url := baseUrl + "?" + params Logger.Trace("DeleteContainer() Api call:", url) req, _ := http.NewRequest("DELETE", url, nil) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() if _, err := ioutil.ReadAll(resp.Body); err != nil { Logger.Error("Could not contact docker endpoint:", endpoint) return nil, err } else { switch resp.StatusCode { case 204: Logger.Info("Container Deleted:", name, ", with ID:", id) return &name, nil case 400: msg := "Bad Api param supplied while trying to delete a container, notify developers." Logger.Error(msg) return nil, errors.New(msg) case 404: Logger.Warn("Container not found, nothing to delete.") return nil, nil case 406: msg := "Container:" + name + " - (" + id + "), is running, cannot delete." Logger.Warn(msg) return nil, errors.New(msg) case 500: msg := "Error while trying to communicate to docker endpoint:" + endpoint // Logger.Error(msg) return nil, errors.New(msg) } statusCode := strconv.Itoa(resp.StatusCode) msg := "API out of sync, contact developers! Status Code:" + statusCode return nil, errors.New(msg) } } if images, err := api.ListContainers(name); err != nil { Logger.Error("Could not get image details:", err) return nil, err } else if images != nil { Logger.Info("Found", len(images), "to delete...") for _, container := range images { Logger.Debug("Trying to delete", container) if deleted, err := delete(container); err != nil { returnError = err msg := "Could not delete container: " + err.Error() Logger.Error(msg) } else if *deleted != "" { deletedContainers = append(deletedContainers, DeletedContainer{*deleted, container}) } } return deletedContainers, returnError } else { Logger.Debug("No containers found for", name) return deletedContainers, returnError } }
func (api *DockerApi) ShowInfo() { Logger.Debug("---------- Test-Flight Docker Info ----------") Logger.Debug("Docker Endpoint:", api.configFile.DockerEndpoint) Logger.Debug("---------------------------------------------") }
func (api *DockerApi) createTestTemplates(options CommandOptions) error { var templateDir = getTemplateDir(api.configFile) var inventory = RequiredFile{ Name: "Test-Flight Test Inventory file", FileName: "inventory", FileType: "f", } var playbook = RequiredFile{ Name: "Test-Flight Test Playbook file", FileName: "playbook.yml", FileType: "f", } // TODO: better to have a type specified that is calculated // much earlier in the process var modeDir = func() string { if options.SingleFileMode { return "filemode" } else { return "dirmode" } }() // --HERE MUST KNOW ABOUT SUB DIR dirmode/filemode of templates // -- needs to work for input and output dir // The directory where the templates used to create inventory and playbook templates := func() string { // could have been simpler but I want to log path at this level var templatePath string if api.configFile.UseSystemDockerTemplates { templatePath = FilePath(api.configFile.TestFlightAssets, "templates", "system") } else { templatePath = FilePath(api.configFile.TestFlightAssets, "templates", "user") } // Logger.Debug("Template dir used for ansible tests:", templatePath) return templatePath }() // This is used below in `ExecuteTemplate()` templateInputDir := FilePath(templates, modeDir) Logger.Debug("Template dir used for generation of ansible files:", templateInputDir) // The directory where to put the generated files templateOutputDir := FilePath(api.meta.Pwd /*api.meta.Dir,*/, templateDir.FileName) Logger.Debug("Template dir used to put generated ansible test files:", templateOutputDir) createFilesFromTemplate := func( templateInputDir string, templateOutputDir string, requiredFile RequiredFile) error { hasFiles, _ := HasRequiredFile(templateOutputDir, requiredFile) if hasFiles && api.configFile.OverwriteTemplates || !hasFiles { fileToCreate := FilePath(templateOutputDir, requiredFile.FileName) var err error file, err := os.Create(fileToCreate) if err != nil { Logger.Error("Error:", err) return err } pattern := filepath.Join(templateInputDir, requiredFile.FileName+"*.tmpl") tmpl := template.Must(template.ParseGlob(pattern)) if err = tmpl.ExecuteTemplate(file, requiredFile.FileName, *api.getTemplateVar()); err != nil { Logger.Error("template execution: %s", err) return err } Logger.Debug("Created file from template:", fileToCreate) } else if hasFiles && !api.configFile.OverwriteTemplates { Logger.Debug(requiredFile.Name, "exists, and system configured to not overwrite.") } return nil } // If the test-flight templates dir doesn't exist, create it. hasFiles, err := HasRequiredFile(api.meta.Dir, templateDir) if !hasFiles { // create if it doesn't exist if _, err = CreateFile(&api.meta.Dir, templateDir); err != nil { return err } } else { Logger.Debug("Required template files already exist. Overwrite set to:", api.configFile.OverwriteTemplates) } err = createFilesFromTemplate(templateInputDir, templateOutputDir, inventory) if err != nil { return err } err = createFilesFromTemplate(templateInputDir, templateOutputDir, playbook) if err != nil { return err } return nil }