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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+**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