From 0c55035ee0f2129a4cb63d07da834dc2210684f1 Mon Sep 17 00:00:00 2001 From: Andrew Rynhard Date: Sun, 4 Jun 2017 08:41:01 -0700 Subject: [PATCH] Initial implementation (#1) --- .dockerignore | 10 ++ .gitignore | 4 + Gopkg.lock | 207 +++++++++++++++++++++++++++++++++++++++ Gopkg.toml | 99 +++++++++++++++++++ README.md | 74 +++++++++++++- cmd/enforce.go | 76 ++++++++++++++ cmd/root.go | 83 ++++++++++++++++ cmd/version.go | 42 ++++++++ conform.yaml | 105 ++++++++++++++++++++ conform/config/config.go | 47 +++++++++ conform/enforce.go | 187 +++++++++++++++++++++++++++++++++++ conform/git/git.go | 88 +++++++++++++++++ conform/version.go | 65 ++++++++++++ main.go | 20 ++++ scripts/deploy.sh | 8 ++ scripts/test.sh | 43 ++++++++ 16 files changed, 1157 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 cmd/enforce.go create mode 100644 cmd/root.go create mode 100644 cmd/version.go create mode 100644 conform.yaml create mode 100644 conform/config/config.go create mode 100644 conform/enforce.go create mode 100644 conform/git/git.go create mode 100644 conform/version.go create mode 100644 main.go create mode 100644 scripts/deploy.sh create mode 100644 scripts/test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c09fff56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.gitignore +conform.yaml +coverage.txt +Dockerfile +docs +examples +LICENSE +Makefile +README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fc9e9f7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +coverage.txt +Dockerfile +docs/build +vendor diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..deb71c23 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,207 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "github.com/Masterminds/semver" + packages = ["."] + revision = "abff1900528dbdaf6f3f5aa92c398be1eaf2a9f7" + version = "v1.3.0" + +[[projects]] + name = "github.com/Masterminds/sprig" + packages = ["."] + revision = "9526be0327b26ad31aa70296a7b10704883976d5" + version = "2.12.0" + +[[projects]] + name = "github.com/Microsoft/go-winio" + packages = ["."] + revision = "f533f7a102197536779ea3a8cb881d639e21ec5a" + version = "v0.4.2" + +[[projects]] + name = "github.com/Sirupsen/logrus" + packages = ["."] + revision = "ba1b36c82c5e05c4f912a88eab0dcd91a171688f" + version = "v0.11.5" + +[[projects]] + name = "github.com/aokoli/goutils" + packages = ["."] + revision = "3391d3790d23d03408670993e957e8f408993c34" + version = "v1.0.1" + +[[projects]] + name = "github.com/docker/distribution" + packages = ["digest","reference"] + revision = "a25b9ef0c9fe242ac04bb20d3a028442b7d266b6" + version = "v2.6.1" + +[[projects]] + name = "github.com/docker/docker" + packages = ["api/types","api/types/blkiodev","api/types/container","api/types/events","api/types/filters","api/types/mount","api/types/network","api/types/reference","api/types/registry","api/types/strslice","api/types/swarm","api/types/time","api/types/versions","api/types/volume","client","pkg/tlsconfig"] + revision = "092cba3727bb9b4a2f0e922cd6c0f93ea270e363" + version = "v1.13.1" + +[[projects]] + name = "github.com/docker/go-connections" + packages = ["nat","sockets","tlsconfig"] + revision = "990a1a1a70b0da4c4cb70e117971a4f0babfbf1a" + version = "v0.2.1" + +[[projects]] + name = "github.com/docker/go-units" + packages = ["."] + revision = "f2d77a61e3c169b43402a0a1e84f06daf29b8190" + version = "v0.3.1" + +[[projects]] + branch = "master" + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "4da3e2cfbabc9f751898f250b49f2439785783a1" + +[[projects]] + branch = "master" + name = "github.com/hashicorp/hcl" + packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] + revision = "392dba7d905ed5d04a5794ba89f558b27e2ba1ca" + +[[projects]] + branch = "master" + name = "github.com/huandu/xstrings" + packages = ["."] + revision = "3959339b333561bf62a38b424fd41517c2c90f40" + +[[projects]] + name = "github.com/imdario/mergo" + packages = ["."] + revision = "3e95a51e0639b4cf372f2ccf74c86749d747fbdc" + version = "0.2.2" + +[[projects]] + branch = "master" + name = "github.com/inconshreveable/mousetrap" + packages = ["."] + revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" + +[[projects]] + branch = "master" + name = "github.com/magiconair/properties" + packages = ["."] + revision = "51463bfca2576e06c62a8504b5c0f06d61312647" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + +[[projects]] + branch = "master" + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "cc8532a8e9a55ea36402aa21efdf403a60d34096" + +[[projects]] + name = "github.com/opencontainers/runc" + packages = ["libcontainer/user"] + revision = "baf6536d6259209c3edfa2b22237af82942d3dfa" + version = "v0.1.1" + +[[projects]] + branch = "master" + name = "github.com/pelletier/go-buffruneio" + packages = ["."] + revision = "c37440a7cf42ac63b919c752ca73a85067e05992" + +[[projects]] + branch = "master" + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "23f644976aa7c724adf4aec911dadf4af17840ab" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "645ef00459ed84a119197bfb8d8205042c6df63d" + version = "v0.8.0" + +[[projects]] + name = "github.com/satori/go.uuid" + packages = ["."] + revision = "879c5887cd475cd7864858769793b2ceb0d44feb" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/spf13/afero" + packages = [".","mem"] + revision = "9be650865eab0c12963d8753212f4f9c66cdcf12" + +[[projects]] + branch = "master" + name = "github.com/spf13/cast" + packages = ["."] + revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" + +[[projects]] + branch = "master" + name = "github.com/spf13/cobra" + packages = ["."] + revision = "1362f95a8d6fe330d00a64380d6e0b65f4992c72" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "8f07c835e5cc1450c082fe3a439cf87b0cbb2d99" + +[[projects]] + branch = "master" + name = "github.com/spf13/pflag" + packages = ["."] + revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" + +[[projects]] + branch = "master" + name = "github.com/spf13/viper" + packages = ["."] + revision = "0967fc9aceab2ce9da34061253ac10fb99bba5b2" + +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["pbkdf2","scrypt"] + revision = "e1a4589e7d3ea14a3352255d04b6f1a418845e5e" + +[[projects]] + branch = "master" + name = "golang.org/x/net" + packages = ["context","context/ctxhttp","proxy"] + revision = "e4fa1c5465ad6111f206fc92186b8c83d64adbe1" + +[[projects]] + branch = "master" + name = "golang.org/x/sys" + packages = ["unix","windows"] + revision = "9ccfe848b9db8435a24c424abbc07a921adf1df5" + +[[projects]] + branch = "master" + name = "golang.org/x/text" + packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] + revision = "470f45bf29f4147d6fbd7dfd0a02a848e49f5bf4" + +[[projects]] + branch = "v2" + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "29c5e22cbb86050d9e51569428deee969beb92788902c1ea0dbfe6ea1ed6f11c" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..0d04d68d --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,99 @@ + +## Gopkg.toml example (these lines may be deleted) + +## "metadata" defines metadata about the project that could be used by other independent +## systems. The metadata defined here will be ignored by dep. +# [metadata] +# key1 = "value that convey data to other systems" +# system1-data = "value that is used by a system" +# system2-data = "value that is used by another system" + +## "required" lists a set of packages (not projects) that must be included in +## Gopkg.lock. This list is merged with the set of packages imported by the current +## project. Use it when your project needs a package it doesn't explicitly import - +## including "main" packages. +# required = ["github.com/user/thing/cmd/thing"] + +## "ignored" lists a set of packages (not projects) that are ignored when +## dep statically analyzes source code. Ignored packages can be in this project, +## or in a dependency. +# ignored = ["github.com/user/project/badpkg"] + +## Constraints are rules for how directly imported projects +## may be incorporated into the depgraph. They are respected by +## dep whether coming from the Gopkg.toml of the current project or a dependency. +# [[constraint]] +## Required: the root import path of the project being constrained. +# name = "github.com/user/project" +# +## Recommended: the version constraint to enforce for the project. +## Only one of "branch", "version" or "revision" can be specified. +# version = "1.0.0" +# branch = "master" +# revision = "abc123" +# +## Optional: an alternate location (URL or import path) for the project's source. +# source = "https://github.com/myfork/package.git" +# +## "metadata" defines metadata about the dependency or override that could be used +## by other independent systems. The metadata defined here will be ignored by dep. +# [metadata] +# key1 = "value that convey data to other systems" +# system1-data = "value that is used by a system" +# system2-data = "value that is used by another system" + +## Overrides have the same structure as [[constraint]], but supersede all +## [[constraint]] declarations from all projects. Only [[override]] from +## the current project's are applied. +## +## Overrides are a sledgehammer. Use them only as a last resort. +# [[override]] +## Required: the root import path of the project being constrained. +# name = "github.com/user/project" +# +## Optional: specifying a version constraint override will cause all other +## constraints on this project to be ignored; only the overridden constraint +## need be satisfied. +## Again, only one of "branch", "version" or "revision" can be specified. +# version = "1.0.0" +# branch = "master" +# revision = "abc123" +# +## Optional: specifying an alternate source location as an override will +## enforce that the alternate location is used for that project, regardless of +## what source location any dependent projects specify. +# source = "https://github.com/myfork/package.git" + + + +[[constraint]] + name = "github.com/Masterminds/semver" + version = "1.3.0" + +[[constraint]] + name = "github.com/Masterminds/sprig" + version = "2.12.0" + +[[constraint]] + name = "github.com/docker/docker" + version = "1.13.1" + +[[constraint]] + branch = "master" + name = "github.com/inconshreveable/mousetrap" + +[[constraint]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + +[[constraint]] + branch = "master" + name = "github.com/spf13/cobra" + +[[constraint]] + branch = "master" + name = "github.com/spf13/viper" + +[[constraint]] + branch = "v2" + name = "gopkg.in/yaml.v2" diff --git a/README.md b/README.md index 818fa834..061c6ef0 100644 --- a/README.md +++ b/README.md @@ -1 +1,73 @@ -# conform \ No newline at end of file +

+

Conform

+

DRY, hygienic, fast builds.

+

+ Gitter + GoDoc +

+

+ Travis + Codecov + Go Report Card +

+

+ Release + GitHub (pre-)release +

+

+ +--- + +**Conform** is a tool for building projects in a flexible and reliabale manner. + +The key features of Conform are: +- **DRY**: Templatized multi-stage Docker builds. +- **Hygienic**: Builds run in Docker. +- **Fast**: Leverages Docker caching, building only what has changed. + +Getting Started +--------------- +Create a file named `conform.yaml` with the following contents: +```yaml +metadata: + repository: example +scripts: + init : | + #!/bin/bash + + set -e + + echo "Hello, world!" +templates: + build: | + FROM alpine:latest as build + RUN echo "Run your build here!" + RUN touch artifact + test: | + FROM alpine:latest as test + COPY --from=build artifact . + RUN echo "Run your tests here!" + image: | + FROM scratch as image + RUN echo "Deply your image here!" + COPY --from=build artifact . +rules: + all: + templates: + - build + - test + - image + +``` + +In the same directory, run: +``` +$ conform enforce all +``` +> **Note:** Conform is still under design. The YAML layout is subject to change. + +Devloping Conform +---------------- + +### License +[![license](https://img.shields.io/github/license/autonomy/conform.svg?style=flat-square)](https://github.com/autonomy/conform/blob/master/LICENSE) diff --git a/cmd/enforce.go b/cmd/enforce.go new file mode 100644 index 00000000..5a06f3b0 --- /dev/null +++ b/cmd/enforce.go @@ -0,0 +1,76 @@ +// Copyright © 2017 NAME HERE +// 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 cmd + +import ( + "context" + "fmt" + + "github.com/Masterminds/semver" + "github.com/autonomy/conform/conform" + "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +// enforceCmd represents the enforce command +var enforceCmd = &cobra.Command{ + Use: "enforce", + Short: "", + Long: ``, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("Invalid argument") + } + err := checkDockerVersion() + if err != nil { + return err + } + + dp := conform.NewExecuter(args[0]) + err = dp.ExecuteRule() + if err != nil { + return err + } + + return nil + }, +} + +func init() { + RootCmd.AddCommand(enforceCmd) + RootCmd.Flags().BoolVar(&debug, "debug", false, "Debug rendering") +} + +func checkDockerVersion() error { + cli, err := client.NewEnvClient() + if err != nil { + return err + } + + serverVersion, err := cli.ServerVersion(context.Background()) + if err != nil { + return err + } + minVersion, err := semver.NewVersion(minDockerVersion) + if err != nil { + return err + } + serverSemVer := semver.MustParse(serverVersion.Version) + i := serverSemVer.Compare(minVersion) + if i < 0 { + return fmt.Errorf("At least Docker version %s is required", minDockerVersion) + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000..16e0fdd2 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,83 @@ +// Copyright © 2017 NAME HERE +// 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 cmd + +import ( + "fmt" + "os" + + homedir "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +const ( + minDockerVersion = "17.05.0-ce" +) + +var ( + target string + imageName string + debug bool +) + +// RootCmd represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "conform", + Short: "DRY, hygienic, fast builds.", + Long: ``, +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.conform.yaml)") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := homedir.Dir() + if err != nil { + fmt.Println(home) + os.Exit(1) + } + + // Search config in home directory with name ".cobra" (without extension). + viper.AddConfigPath(home) + viper.SetConfigName(".cobra") + } + + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 00000000..a9c69abc --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,42 @@ +// Copyright © 2017 NAME HERE +// 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 cmd + +import ( + "github.com/autonomy/conform/conform" + "github.com/spf13/cobra" +) + +var ( + shortVersion bool +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Prints the version", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + if shortVersion { + conform.PrintShortVersion() + } else { + conform.PrintLongVersion() + } + }, +} + +func init() { + versionCmd.Flags().BoolVar(&shortVersion, "short", false, "Print the short version") + RootCmd.AddCommand(versionCmd) +} diff --git a/conform.yaml b/conform.yaml new file mode 100644 index 00000000..282211d8 --- /dev/null +++ b/conform.yaml @@ -0,0 +1,105 @@ +metadata: + repository: autonomy/conform + +default: image + +scripts: + init : | + #!/bin/bash + + set -e + + BUILDDEPS=( github.com/golang/dep/cmd/dep ) + for b in ${BUILDDEPS[@]}; do + echo "Installing $b" + go get -u $b + done + + test_artifacts: | + #!/bin/bash + + set -e + + docker run --rm -i --volume $(pwd):/out ${CONFORM_IMAGE} cp coverage.txt /out + + deploy: | + #!/bin/bash + + set -e + + if [ ${CONFORM_IS_DIRTY} == "true" ]; then + echo "The working tree is dirty, aborting ..." + exit 1 + fi + + docker login -u "${DOCKER_USERNAME}" -p "${DOCKER_PASSWORD}" + docker push ${CONFORM_IMAGE} + + if [ ${CONFORM_IS_TAG} == "true" ]; then + docker tag ${CONFORM_IMAGE} autonomy/conform:${CONFORM_TAG} + docker push autonomy/conform:${CONFORM_TAG} + if [ ${CONFORM_PRERELEASE} == "" ]; then + docker tag ${CONFORM_IMAGE} autonomy/conform:latest + docker push autonomy/conform:latest + fi + fi + + clean: | + dep ensure + dep prune + +templates: + build: | + FROM golang:1.8.3 as build + WORKDIR /go/src/github.com/autonomy/conform + COPY ./ ./ + RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /conform -a -ldflags "-X \"github.com/autonomy/conform/version.Tag={{ trimAll "v" .GitInfo.Tag }}\" -X \"github.com/autonomy/conform/version.SHA={{ .GitInfo.SHA }}\" -X \"github.com/autonomy/conform/version.Built={{ .Built }}\"" + test: | + FROM golang:1.8.3 as test + MAINTAINER Andrew Rynhard + WORKDIR /go/src/github.com/autonomy/conform + RUN go get -u github.com/golang/lint/golint + COPY --from=build /go/src/github.com/autonomy/conform . + RUN chmod +x ./scripts/test.sh; sync; ./scripts/test.sh + image: | + FROM alpine:3.6 as image + MAINTAINER Andrew Rynhard + RUN apk --update add bash \ + && rm -rf /var/cache/apk/* + WORKDIR /app + COPY --from=build /go/src/github.com/autonomy/conform/conform . + ENTRYPOINT ["./conform"] + +rules: + build: + templates: + - build + + test: + templates: + - build + - test + after: + - test_artifacts + + image: + templates: + - build + - image + + all: + before: + - init + - clean + templates: + - build + - test + - image + + deploy: + templates: + - build + - test + - image + after: + - deploy diff --git a/conform/config/config.go b/conform/config/config.go new file mode 100644 index 00000000..a161c237 --- /dev/null +++ b/conform/config/config.go @@ -0,0 +1,47 @@ +package config + +import ( + "fmt" + "io/ioutil" + + yaml "gopkg.in/yaml.v2" +) + +// Config represents the YAML. +type Config struct { + Debug bool + Default *string `yaml:"default"` + Metadata *Metadata `yaml:"metadata"` + Scripts map[string]string `yaml:"scripts"` + Templates map[string]string `yaml:"templates"` + Rules map[string]*Rule `yaml:"rules"` +} + +// Metadata contains metadata. +type Metadata struct { + Repository *string `yaml:"repository"` + Registry *string `yaml:"registry"` +} + +// Rule contains rules. +type Rule struct { + Templates []string `yaml:"templates"` + Artifacts []string `yaml:"artifacts"` + Before []string `yaml:"before"` + After []string `yaml:"after"` +} + +// NewConfig instantiates and returns a config. +func NewConfig() *Config { + rBytes, err := ioutil.ReadFile("conform.yaml") + if err != nil { + fmt.Printf("Unable to load conform.yaml: %v", err) + } + c := Config{} + err = yaml.Unmarshal(rBytes, &c) + if err != nil { + fmt.Printf("Unable to load conform.yaml: %v", err) + } + + return &c +} diff --git a/conform/enforce.go b/conform/enforce.go new file mode 100644 index 00000000..4f190756 --- /dev/null +++ b/conform/enforce.go @@ -0,0 +1,187 @@ +package conform + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" + "text/template" + + "github.com/Masterminds/sprig" + "github.com/autonomy/conform/conform/config" + "github.com/autonomy/conform/conform/git" +) + +// Executer performs all the build actions for a rule. +type Executer struct { + config *config.Config + rule string + GitInfo *git.Info + Built string +} + +// NewExecuter instantiates and returns an executer. +func NewExecuter(rule string) *Executer { + e := &Executer{} + gitInfo := git.NewInfo() + date := []byte{} + if gitInfo.IsTag { + _date, err := exec.Command("date").Output() + if err != nil { + fmt.Print(err) + } + + date = _date + } + + c := config.NewConfig() + e.config = c + e.GitInfo = gitInfo + e.Built = strings.TrimSuffix(string(date), "\n") + e.rule = rule + + return e +} + +// ExecuteBuild executes a docker build. +func (e *Executer) ExecuteBuild() error { + image := e.FormatImageNameSHA() + if e.GitInfo.IsDirty { + image = e.FormatImageNameDirty() + } + + os.Setenv("CONFORM_IMAGE", image) + + args := append([]string{"build", "--tag", image, "."}) + command := exec.Command("docker", args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Start() + err := command.Wait() + if err != nil { + return err + } + + return nil +} + +// RenderDockerfile writes the final Dockerfile to disk. +func (e *Executer) RenderDockerfile(target *config.Rule) error { + dockerfile := `# THIS FILE IS AUTOGENERATED BY CONFORM. DO NOT EDIT! +# Note: This file should be ignored by git. + ` + for _, p := range target.Templates { + r, err := e.RenderTemplate(p) + if err != nil { + return err + } + dockerfile = dockerfile + "\n" + *r + } + + if e.config.Debug { + fmt.Println(dockerfile) + } else { + ioutil.WriteFile("Dockerfile", []byte(dockerfile), 0644) + } + + return nil +} + +// RenderTemplate executes the template and returns it. +func (e *Executer) RenderTemplate(s string) (*string, error) { + if _s, ok := e.config.Templates[s]; ok { + var wr bytes.Buffer + tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(_s) + if err != nil { + return nil, err + } + + err = tmpl.Execute(&wr, &e) + if err != nil { + return nil, err + } + + str := wr.String() + return &str, nil + } + + return nil, fmt.Errorf("Template %q is not defined in conform.yaml", s) +} + +// ExtractArtifact copies an artifact from a build. +func (e *Executer) ExtractArtifact(artifact string) error { + return fmt.Errorf("Artifact %q is not defined in conform.yaml", artifact) +} + +// ExecuteScript executes a script for a rule. +func (e *Executer) ExecuteScript(script string) error { + if s, ok := e.config.Scripts[script]; ok { + log.Printf("Running %s script", script) + command := exec.Command("bash", "-c", s) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Start() + err := command.Wait() + if err != nil { + return err + } + + return nil + } + + return fmt.Errorf("Script %q is not defined in conform.yaml", script) +} + +// ExecuteRule performs all the relevant actions specified in its' declaration. +func (e *Executer) ExecuteRule() error { + if t, ok := e.config.Rules[e.rule]; ok { + log.Printf("Executing %q", e.rule) + for _, s := range t.Before { + err := e.ExecuteScript(s) + if err != nil { + return err + } + } + err := e.RenderDockerfile(t) + if err != nil { + return err + } + err = e.ExecuteBuild() + if err != nil { + return err + } + for _, s := range t.After { + err := e.ExecuteScript(s) + if err != nil { + return err + } + } + + return nil + } + + return fmt.Errorf("Rule %q is not defined in conform.yaml", e.rule) +} + +// FormatImageNameDirty formats the image name. +func (e *Executer) FormatImageNameDirty() string { + return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, "dirty") +} + +// FormatImageNameSHA formats the image name. +func (e *Executer) FormatImageNameSHA() string { + return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, e.GitInfo.SHA) +} + +// FormatImageNameTag formats the image name. +func (e *Executer) FormatImageNameTag() string { + return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, e.GitInfo.Tag) +} + +// FormatImageNameLatest formats the image name. +func (e *Executer) FormatImageNameLatest() string { + return fmt.Sprintf("%s:%s", *e.config.Metadata.Repository, "latest") +} diff --git a/conform/git/git.go b/conform/git/git.go new file mode 100644 index 00000000..a430f4d1 --- /dev/null +++ b/conform/git/git.go @@ -0,0 +1,88 @@ +package git + +import ( + "fmt" + "log" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/Masterminds/semver" +) + +// Info contains the status of the working tree. +type Info struct { + Branch string + SHA string + Tag string + Prerelease string + IsTag bool + IsDirty bool +} + +// NewInfo instantiates and returns info. +func NewInfo() *Info { + branch, err := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD").Output() + if err != nil { + log.Fatalf("Failed to get branch [%v]", err) + } + sha, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() + if err != nil { + log.Fatalf("Failed to get sha [%v]", err) + } + isTag := false + _, err = exec.Command("git", "symbolic-ref", "HEAD").Output() + if err != nil { + isTag = true + } + tag := "undefined" + if isTag { + _tag, err2 := exec.Command("git", "describe", "--exact-match", "HEAD").Output() + if err2 == nil { + tag = string(_tag) + } + } + status, err := exec.Command("git", "status", "--porcelain").Output() + if err != nil { + log.Fatal(err) + } + isDirty := false + if strings.TrimSuffix(string(status), "\n") != "" { + isDirty = true + } + + prerelease := "" + if isTag { + sv, err := semver.NewVersion(strings.TrimSuffix(string(tag[1:]), "\n")) + if err != nil { + log.Fatal(err) + } + + prerelease = sv.Prerelease() + } + + fmt.Printf("Branch: %s\n", strings.TrimSuffix(string(branch), "\n")) + os.Setenv("CONFORM_BRANCH", strings.TrimSuffix(string(branch), "\n")) + fmt.Printf("SHA: %s\n", strings.TrimSuffix(string(sha), "\n")) + os.Setenv("CONFORM_SHA", strings.TrimSuffix(string(sha), "\n")) + fmt.Printf("Tag: %s\n", strings.TrimSuffix(string(tag), "\n")) + os.Setenv("CONFORM_TAG", strings.TrimSuffix(string(tag), "\n")) + fmt.Printf("Status: %s\n", strings.TrimSuffix(string(status), "\n")) + fmt.Printf("IsTag: %v\n", isTag) + os.Setenv("CONFORM_IS_TAG", strconv.FormatBool(isTag)) + fmt.Printf("Prerelease: %s\n", prerelease) + os.Setenv("CONFORM_PRERELEASE", prerelease) + fmt.Printf("IsDirty: %v\n", isDirty) + os.Setenv("CONFORM_IS_DIRTY", strconv.FormatBool(isDirty)) + // os.Setenv("CONFORM_IMAGE", strconv.FormatBool(isTag)) + + return &Info{ + Branch: strings.TrimSuffix(string(branch), "\n"), + SHA: strings.TrimSuffix(string(sha), "\n"), + Tag: strings.TrimSuffix(string(tag), "\n"), + Prerelease: prerelease, + IsTag: isTag, + IsDirty: isDirty, + } +} diff --git a/conform/version.go b/conform/version.go new file mode 100644 index 00000000..a9844762 --- /dev/null +++ b/conform/version.go @@ -0,0 +1,65 @@ +package conform + +import ( + "bytes" + "fmt" + "runtime" + "text/template" +) + +var ( + // Tag is set at build time. + Tag string + // SHA is set at build time. + SHA string + // Built is set at build time. + Built string +) + +const versionTemplate = `Devise: + Tag: {{ .Tag }} + SHA: {{ .SHA }} + Built: {{ .Built }} + Go version: {{ .GoVersion }} + OS/Arch: {{ .Os }}/{{ .Arch }} +` + +// Version contains verbose version information. +type Version struct { + Tag string + SHA string + Built string + GoVersion string + Os string + Arch string +} + +// PrintLongVersion prints verbose version information. +func PrintLongVersion() { + v := Version{ + Tag: Tag, + SHA: SHA, + GoVersion: runtime.Version(), + Os: runtime.GOOS, + Arch: runtime.GOARCH, + Built: Built, + } + + var wr bytes.Buffer + tmpl, err := template.New("version").Parse(versionTemplate) + if err != nil { + fmt.Println(err) + } + + err = tmpl.Execute(&wr, v) + if err != nil { + fmt.Println(err) + } + + fmt.Println(wr.String()) +} + +// PrintShortVersion prints the tag and sha. +func PrintShortVersion() { + fmt.Println(fmt.Sprintf("Devise %s-%s", Tag, SHA)) +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..0c4004f1 --- /dev/null +++ b/main.go @@ -0,0 +1,20 @@ +// Copyright © 2017 NAME HERE +// 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 main + +import "github.com/autonomy/conform/cmd" + +func main() { + cmd.Execute() +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 00000000..e1caa824 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +# The deploy stage within Travis-CI dirtys the working tree. This will cause +# the docker tagging to fail. Reset to HEAD as a workaround. +git reset --hard HEAD +conform build deploy diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100644 index 00000000..23ac518f --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +GOPACKAGES=$(go list ./... | grep -v /vendor/ | grep -v /api) +GOFILES=$(find . -type f -name '*.go' -not -path "./vendor/*" -not -path "./api/*") + +COVERAGE=coverage.txt +PROFILE=profile.out + +if [[ -f ${COVERAGE} ]]; then + rm ${COVERAGE} +fi + +touch ${COVERAGE} + +echo "Running tests" +for package in ${GOPACKAGES[@]}; do + go test -race -coverprofile=${PROFILE} -covermode=atomic $package + if [ -f ${PROFILE} ]; then + cat ${PROFILE} >> ${COVERAGE} + rm ${PROFILE} + fi +done + +echo "Vetting packages" +go vet ${GOPACKAGES} +if [ $? -eq 1 ]; then + exit 1 +fi + +echo "Linting packages" +golint -set_exit_status ${GOPACKAGES} +if [ $? -eq 1 ]; then + exit 1 +fi + +echo "Formatting go files" +if [ ! -z "$(gofmt -l -s ${GOFILES})" ]; then + exit 1 +fi + +exit 0