├── .gitignore
├── brewkit.jsonnet
├── Dockerfile
├── docs
├── img
│ ├── architecture-pkg.png
│ └── architecture-overview.png
├── architecture.md
├── readme.md
├── config
│ └── overview.md
├── cli
│ └── overview.md
├── build-definition
│ ├── jsonnet-extensions.md
│ ├── overview.md
│ └── reference.md
├── diagrams
│ └── architecture.drawio.xml
├── adr
│ ├── 001-buildkit.md
│ └── 002-jsonnet.md
└── features.md
├── internal
├── frontend
│ ├── app
│ │ ├── version
│ │ │ └── version.go
│ │ ├── buildconfig
│ │ │ ├── constants.go
│ │ │ ├── parser.go
│ │ │ └── config.go
│ │ ├── reporter
│ │ │ └── reporter.go
│ │ ├── config
│ │ │ ├── config.go
│ │ │ ├── parser.go
│ │ │ └── defaults.go
│ │ ├── builddefinition
│ │ │ ├── errors.go
│ │ │ ├── model.go
│ │ │ ├── trace.go
│ │ │ ├── builder.go
│ │ │ └── vertex.go
│ │ └── service
│ │ │ ├── cache.go
│ │ │ └── build.go
│ └── infrastructure
│ │ ├── config
│ │ ├── model.go
│ │ └── parser.go
│ │ ├── jsonnet
│ │ └── formatter.go
│ │ └── builddefinition
│ │ ├── funcs.go
│ │ ├── model.go
│ │ ├── nativefunc.go
│ │ └── parser.go
├── backend
│ ├── app
│ │ ├── ssh
│ │ │ └── agentprovider.go
│ │ ├── reporter
│ │ │ └── reporter.go
│ │ ├── docker
│ │ │ ├── errors.go
│ │ │ └── client.go
│ │ ├── cache
│ │ │ └── service.go
│ │ ├── dockerfile
│ │ │ ├── vargenerator.go
│ │ │ └── targetgenerator.go
│ │ └── build
│ │ │ └── service.go
│ ├── api
│ │ ├── api.go
│ │ └── model.go
│ └── infrastructure
│ │ ├── ssh
│ │ └── agentprovider.go
│ │ └── docker
│ │ ├── outputparser.go
│ │ └── client.go
├── common
│ ├── infrastructure
│ │ ├── executor
│ │ │ ├── logger.go
│ │ │ ├── args.go
│ │ │ ├── opt.go
│ │ │ └── executor.go
│ │ └── logger
│ │ │ ├── executorlogger.go
│ │ │ └── logger.go
│ ├── maybe
│ │ ├── fmt.go
│ │ ├── json.go
│ │ └── maybe.go
│ ├── slices
│ │ ├── index.go
│ │ ├── diff.go
│ │ └── map.go
│ ├── maps
│ │ ├── maps.go
│ │ └── set.go
│ └── either
│ │ ├── json.go
│ │ └── either.go
└── dockerfile
│ └── dockerfile.go
├── brewkit
├── images.libsonnet
└── project.libsonnet
├── go.mod
├── data
└── specification
│ ├── config
│ └── v1.json
│ └── build-definition
│ └── v1.json
├── cmd
└── brewkit
│ ├── fmt.go
│ ├── version.go
│ ├── cache.go
│ ├── common.go
│ ├── config.go
│ ├── main.go
│ └── build.go
├── .golangci.yml
├── LICENSE
├── go.sum
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /.vscode/
3 | /bin/brewkit
4 | vendor
5 |
6 |
--------------------------------------------------------------------------------
/brewkit.jsonnet:
--------------------------------------------------------------------------------
1 | local project = import 'brewkit/project.libsonnet';
2 |
3 | project.project()
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM docker:20.10
2 |
3 | COPY bin/brewkit /usr/local/bin/
4 |
5 | ENTRYPOINT ["brewkit"]
--------------------------------------------------------------------------------
/docs/img/architecture-pkg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ispringtech/brewkit/HEAD/docs/img/architecture-pkg.png
--------------------------------------------------------------------------------
/internal/frontend/app/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | const (
4 | APIVersionV1 = "brewkit/v1"
5 | )
6 |
--------------------------------------------------------------------------------
/docs/img/architecture-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ispringtech/brewkit/HEAD/docs/img/architecture-overview.png
--------------------------------------------------------------------------------
/internal/backend/app/ssh/agentprovider.go:
--------------------------------------------------------------------------------
1 | package ssh
2 |
3 | type AgentProvider interface {
4 | Default() string
5 | }
6 |
--------------------------------------------------------------------------------
/internal/frontend/app/buildconfig/constants.go:
--------------------------------------------------------------------------------
1 | package buildconfig
2 |
3 | const (
4 | DefaultName = "brewkit.jsonnet"
5 | )
6 |
--------------------------------------------------------------------------------
/internal/common/infrastructure/executor/logger.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | type Logger interface {
4 | Info(s string)
5 | Debug(s string)
6 | }
7 |
--------------------------------------------------------------------------------
/brewkit/images.libsonnet:
--------------------------------------------------------------------------------
1 | {
2 | golang: "golang:1.20",
3 | golangcilint: "golangci/golangci-lint:v1.53",
4 |
5 | dockerfile: "docker/dockerfile:1.4"
6 | }
--------------------------------------------------------------------------------
/internal/backend/app/reporter/reporter.go:
--------------------------------------------------------------------------------
1 | package reporter
2 |
3 | type Reporter interface {
4 | Logf(format string, a ...any)
5 | Debugf(format string, a ...any)
6 | }
7 |
--------------------------------------------------------------------------------
/internal/frontend/app/reporter/reporter.go:
--------------------------------------------------------------------------------
1 | package reporter
2 |
3 | type Reporter interface {
4 | Logf(format string, a ...any)
5 | Debugf(format string, a ...any)
6 | }
7 |
--------------------------------------------------------------------------------
/internal/frontend/app/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Config struct {
4 | Secrets []Secret
5 | }
6 |
7 | type Secret struct {
8 | ID string
9 | Path string
10 | }
11 |
--------------------------------------------------------------------------------
/internal/frontend/app/builddefinition/errors.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | import (
4 | "github.com/pkg/errors"
5 | )
6 |
7 | var (
8 | ErrUnsupportedAPIVersion = errors.New("unsupported API version")
9 | )
10 |
--------------------------------------------------------------------------------
/internal/frontend/infrastructure/config/model.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | type Config struct {
4 | Secrets []Secret `json:"secrets"`
5 | }
6 |
7 | type Secret struct {
8 | ID string `json:"id"`
9 | Path string `json:"path"`
10 | }
11 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 |
3 | ## BrewKit context level architecture overview
4 |
5 | 
6 |
7 | ## Brewkit component level architecture
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/internal/frontend/app/buildconfig/parser.go:
--------------------------------------------------------------------------------
1 | package buildconfig
2 |
3 | type Parser interface {
4 | Parse(path string) (Config, error)
5 | // CompileConfig templates config file and returns it raw without parsing
6 | CompileConfig(configPath string) (string, error)
7 | }
8 |
--------------------------------------------------------------------------------
/internal/common/infrastructure/executor/args.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | type Args []string
4 |
5 | func (args *Args) AddKV(key, value string) {
6 | *args = append(*args, key, value)
7 | }
8 |
9 | func (args *Args) AddArgs(arguments ...string) {
10 | *args = append(*args, arguments...)
11 | }
12 |
--------------------------------------------------------------------------------
/docs/readme.md:
--------------------------------------------------------------------------------
1 | # BrewKit documentation
2 |
3 | * [**Build definition overview**](build-definition/overview.md)
4 | * [**Build definition reference**](build-definition/reference.md)
5 |
6 | * [**Host config**](config/overview.md)
7 | * [**CLI**](cli/overview.md)
8 |
9 | * [Architecture](architecture.md)
10 |
--------------------------------------------------------------------------------
/internal/frontend/app/config/parser.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | stderrors "errors"
5 | )
6 |
7 | var (
8 | ErrConfigNotFound = stderrors.New("config not found")
9 | )
10 |
11 | type Parser interface {
12 | Config(configPath string) (Config, error)
13 | Dump(config Config) ([]byte, error)
14 | }
15 |
--------------------------------------------------------------------------------
/internal/common/maybe/fmt.go:
--------------------------------------------------------------------------------
1 | package maybe
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | func (m Maybe[T]) String() string {
8 | if !Valid(m) {
9 | return ""
10 | }
11 |
12 | // Convert to empty interface to make the go compiler satisfied
13 | var v interface{} = Just(m)
14 |
15 | return fmt.Sprintf("%s", v)
16 | }
17 |
--------------------------------------------------------------------------------
/internal/common/slices/index.go:
--------------------------------------------------------------------------------
1 | package slices
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/maybe"
5 | )
6 |
7 | func Find[T any](s []T, f func(T) bool) maybe.Maybe[T] {
8 | for _, v := range s {
9 | if f(v) {
10 | return maybe.NewJust(v)
11 | }
12 | }
13 |
14 | return maybe.NewNone[T]()
15 | }
16 |
--------------------------------------------------------------------------------
/internal/backend/app/docker/errors.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type RequestError struct {
8 | Output string
9 | Code int
10 | }
11 |
12 | func (e RequestError) Error() string {
13 | msg := fmt.Sprintf("request to docker client failed: code %d\n", e.Code)
14 | if e.Output != "" {
15 | msg += fmt.Sprintf("%s\n", e.Output)
16 | }
17 |
18 | return msg
19 | }
20 |
--------------------------------------------------------------------------------
/internal/common/maps/maps.go:
--------------------------------------------------------------------------------
1 | package maps
2 |
3 | func FromSlice[K comparable, V, E any](s []V, f func(V) (K, E)) map[K]E {
4 | res := map[K]E{}
5 | for _, v := range s {
6 | k, newV := f(v)
7 | res[k] = newV
8 | }
9 | return res
10 | }
11 |
12 | func ToSlice[K comparable, V, E any](m map[K]E, f func(K, E) V) []V {
13 | res := make([]V, 0, len(m))
14 | for k, e := range m {
15 | res = append(res, f(k, e))
16 | }
17 | return res
18 | }
19 |
--------------------------------------------------------------------------------
/internal/backend/api/api.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "context"
5 | )
6 |
7 | type BuildParams struct {
8 | ForcePull bool
9 | }
10 |
11 | type ClearParams struct {
12 | All bool
13 | }
14 |
15 | type BuilderAPI interface {
16 | Build(ctx context.Context, v Vertex, vars []Var, secretsSrc []SecretSrc, params BuildParams) error
17 | }
18 |
19 | type CacheAPI interface {
20 | ClearCache(ctx context.Context, params ClearParams) error
21 | }
22 |
--------------------------------------------------------------------------------
/internal/common/maybe/json.go:
--------------------------------------------------------------------------------
1 | package maybe
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | func (m *Maybe[T]) UnmarshalJSON(bytes []byte) error {
8 | var v T
9 | err := json.Unmarshal(bytes, &v)
10 | if err != nil {
11 | return err
12 | }
13 |
14 | m.v = v
15 | m.valid = true
16 |
17 | return nil
18 | }
19 |
20 | func (m Maybe[T]) MarshalJSON() ([]byte, error) {
21 | if !m.valid {
22 | return []byte("null"), nil
23 | }
24 | return json.Marshal(m.v)
25 | }
26 |
--------------------------------------------------------------------------------
/internal/frontend/app/config/defaults.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/pkg/errors"
8 | )
9 |
10 | const (
11 | defaultConfigPath = "%s/.brewkit/config"
12 | )
13 |
14 | var (
15 | DefaultConfig = Config{}
16 | )
17 |
18 | func DefaultConfigPath() (string, error) {
19 | homeDir, err := os.UserHomeDir()
20 | if err != nil {
21 | return "", errors.Wrap(err, "failed to receive user home dir")
22 | }
23 |
24 | return fmt.Sprintf(defaultConfigPath, homeDir), nil
25 | }
26 |
--------------------------------------------------------------------------------
/internal/common/maps/set.go:
--------------------------------------------------------------------------------
1 | package maps
2 |
3 | // Set on map with empty struct as value
4 | type Set[T comparable] map[T]struct{}
5 |
6 | func (s *Set[T]) Add(v T) {
7 | (*s)[v] = struct{}{}
8 | }
9 |
10 | func (s *Set[T]) Remove(v T) {
11 | delete(*s, v)
12 | }
13 |
14 | func (s *Set[T]) Has(v T) bool {
15 | _, has := (*s)[v]
16 | return has
17 | }
18 |
19 | func SetFromSlice[T any, E comparable](s []T, f func(T) E) Set[E] {
20 | return FromSlice(s, func(v T) (E, struct{}) {
21 | return f(v), struct{}{}
22 | })
23 | }
24 |
--------------------------------------------------------------------------------
/internal/common/infrastructure/logger/executorlogger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/infrastructure/executor"
5 | )
6 |
7 | func NewExecutorLogger(logger Logger) executor.Logger {
8 | return &executorLogger{logger: logger}
9 | }
10 |
11 | type executorLogger struct {
12 | logger Logger
13 | }
14 |
15 | func (logger *executorLogger) Info(s string) {
16 | logger.logger.Logf(s)
17 | }
18 |
19 | func (logger *executorLogger) Debug(s string) {
20 | logger.logger.Debugf(s)
21 | }
22 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ispringtech/brewkit
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/google/go-jsonnet v0.20.0
7 | github.com/pkg/errors v0.9.1
8 | github.com/urfave/cli/v2 v2.25.1
9 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb
10 | )
11 |
12 | require (
13 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
14 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
15 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
16 | gopkg.in/yaml.v2 v2.2.7 // indirect
17 | sigs.k8s.io/yaml v1.1.0 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/internal/backend/app/cache/service.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/ispringtech/brewkit/internal/backend/api"
7 | "github.com/ispringtech/brewkit/internal/backend/app/docker"
8 | )
9 |
10 | func NewCacheService(dockerClient docker.Client) api.CacheAPI {
11 | return &cacheService{dockerClient: dockerClient}
12 | }
13 |
14 | type cacheService struct {
15 | dockerClient docker.Client
16 | }
17 |
18 | func (service *cacheService) ClearCache(ctx context.Context, params api.ClearParams) error {
19 | return service.dockerClient.ClearCache(ctx, docker.ClearCacheParams{
20 | All: params.All,
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/internal/frontend/app/builddefinition/model.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | import (
4 | stdslices "golang.org/x/exp/slices"
5 |
6 | "github.com/ispringtech/brewkit/internal/backend/api"
7 | "github.com/ispringtech/brewkit/internal/common/maybe"
8 | )
9 |
10 | type Definition struct {
11 | Vertexes []api.Vertex
12 | Vars []api.Var
13 | }
14 |
15 | func (d Definition) Vertex(name string) maybe.Maybe[api.Vertex] {
16 | i := stdslices.IndexFunc(d.Vertexes, func(vertex api.Vertex) bool {
17 | return vertex.Name == name
18 | })
19 | if i == -1 {
20 | return maybe.Maybe[api.Vertex]{}
21 | }
22 |
23 | return maybe.NewJust(d.Vertexes[i])
24 | }
25 |
--------------------------------------------------------------------------------
/internal/common/slices/diff.go:
--------------------------------------------------------------------------------
1 | package slices
2 |
3 | // Diff return diff elements from slices
4 | func Diff[T comparable](s1, s2 []T) []T {
5 | s1Map := map[T]struct{}{}
6 | for _, element := range s1 {
7 | s1Map[element] = struct{}{}
8 | }
9 | s2Map := map[T]struct{}{}
10 | for _, element := range s2 {
11 | s2Map[element] = struct{}{}
12 | }
13 |
14 | var res []T
15 | for _, element := range s2 {
16 | if _, exists := s1Map[element]; !exists {
17 | res = append(res, element)
18 | }
19 | }
20 | for _, element := range s1 {
21 | if _, exists := s2Map[element]; !exists {
22 | res = append(res, element)
23 | }
24 | }
25 | return res
26 | }
27 |
--------------------------------------------------------------------------------
/internal/frontend/app/service/cache.go:
--------------------------------------------------------------------------------
1 | package service
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/ispringtech/brewkit/internal/backend/api"
7 | )
8 |
9 | type ClearCacheParam struct {
10 | All bool
11 | }
12 |
13 | type Cache interface {
14 | ClearCache(ctx context.Context, param ClearCacheParam) error
15 | }
16 |
17 | func NewCacheService(cacheAPI api.CacheAPI) Cache {
18 | return &cacheService{cacheAPI: cacheAPI}
19 | }
20 |
21 | type cacheService struct {
22 | cacheAPI api.CacheAPI
23 | }
24 |
25 | func (service *cacheService) ClearCache(ctx context.Context, param ClearCacheParam) error {
26 | return service.cacheAPI.ClearCache(ctx, api.ClearParams{
27 | All: param.All,
28 | })
29 | }
30 |
--------------------------------------------------------------------------------
/internal/backend/infrastructure/ssh/agentprovider.go:
--------------------------------------------------------------------------------
1 | package ssh
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/pkg/errors"
7 |
8 | "github.com/ispringtech/brewkit/internal/backend/app/ssh"
9 | )
10 |
11 | const (
12 | sshAuthSock = "SSH_AUTH_SOCK"
13 | )
14 |
15 | func NewAgentProvider() (ssh.AgentProvider, error) {
16 | socket, found := os.LookupEnv(sshAuthSock)
17 | if !found {
18 | return nil, errors.Errorf("ssh auth socket via env %s not found", sshAuthSock)
19 | }
20 |
21 | return &agentProvider{defaultAgent: socket}, nil
22 | }
23 |
24 | type agentProvider struct {
25 | defaultAgent string
26 | }
27 |
28 | func (provider agentProvider) Default() string {
29 | return provider.defaultAgent
30 | }
31 |
--------------------------------------------------------------------------------
/internal/frontend/infrastructure/jsonnet/formatter.go:
--------------------------------------------------------------------------------
1 | package jsonnet
2 |
3 | import (
4 | "os"
5 | "path"
6 |
7 | jsonnetformatter "github.com/google/go-jsonnet/formatter"
8 | "github.com/pkg/errors"
9 | )
10 |
11 | type Formatter struct{}
12 |
13 | func (formatter Formatter) Format(configPath string) (string, error) {
14 | filename := path.Base(configPath)
15 | configData, err := os.ReadFile(configPath)
16 | if err != nil {
17 | return "", err
18 | }
19 |
20 | options := jsonnetformatter.DefaultOptions()
21 | options.Indent = 4
22 | options.StringStyle = jsonnetformatter.StringStyleLeave
23 |
24 | data, err := jsonnetformatter.Format(filename, string(configData), options)
25 | return data, errors.Wrap(err, "failed to format config file")
26 | }
27 |
--------------------------------------------------------------------------------
/internal/common/either/json.go:
--------------------------------------------------------------------------------
1 | package either
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | func (e *Either[L, R]) UnmarshalJSON(bytes []byte) error {
9 | l := new(L)
10 | lErr := json.Unmarshal(bytes, &l)
11 | if lErr == nil {
12 | *e = NewLeft[L, R](*l)
13 | return nil
14 | }
15 |
16 | r := new(R)
17 | rErr := json.Unmarshal(bytes, &r)
18 | if rErr == nil {
19 | *e = NewRight[L, R](*r)
20 | return nil
21 | }
22 |
23 | return fmt.Errorf("failed to unmarshal either to %T or %T", l, r)
24 | }
25 |
26 | func (e Either[L, R]) MarshalJSON() (res []byte, err error) {
27 | e.
28 | MapLeft(func(l L) {
29 | res, err = json.Marshal(l)
30 | }).
31 | MapRight(func(r R) {
32 | res, err = json.Marshal(r)
33 | })
34 |
35 | return res, err
36 | }
37 |
--------------------------------------------------------------------------------
/data/specification/config/v1.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "BrewKit config",
3 | "type": "object",
4 | "properties": {
5 | "secrets": {
6 | "type": "array",
7 | "items": {
8 | "$ref": "#/$defs/secret"
9 | }
10 | }
11 | },
12 |
13 | "$defs": {
14 | "secret": {
15 | "type": "object",
16 | "properties": {
17 | "id": {
18 | "description": "Unique secret id",
19 | "type": "string"
20 | },
21 | "path": {
22 | "description": "Path to secret on host",
23 | "type": "string"
24 | }
25 | },
26 | "required": [ "id", "path" ]
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/docs/config/overview.md:
--------------------------------------------------------------------------------
1 | # BrewKit host config
2 |
3 | Config used to configure brewkit host preferences
4 |
5 | ## Location
6 |
7 | By default, brewkit trying to find config in `${HOME}/.brewkit/config` or in `$BREWKIT_CONFIG`. If there is no such path, used empty config.
8 | There is no config auto creation
9 |
10 |
11 | ## Reference
12 |
13 | [Schema v1](/data/specification/config/v1.json)
14 |
15 | ### Secrets
16 |
17 | Define secret to use in build-definition. See [secrets in build-definition](/docs/build-definition/reference.md#secrets)
18 |
19 | ```jsonnet
20 | {
21 | "secrets": [
22 | {
23 | // unique id for secret
24 | "id": "aws",
25 | // path may contain env variables
26 | "path": "${HOME}/.aws/credentials"
27 | },
28 | ]
29 | }
30 | ```
--------------------------------------------------------------------------------
/cmd/brewkit/fmt.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/urfave/cli/v2"
7 |
8 | "github.com/ispringtech/brewkit/internal/frontend/infrastructure/jsonnet"
9 | )
10 |
11 | func fmtCommand() *cli.Command {
12 | return &cli.Command{
13 | Name: "fmt",
14 | Usage: "jsonnetfmt passed files",
15 | Action: executeFmt,
16 | }
17 | }
18 |
19 | func executeFmt(ctx *cli.Context) error {
20 | formatter := jsonnet.Formatter{}
21 |
22 | for _, filepath := range ctx.Args().Slice() {
23 | fileInfo, err := os.Stat(filepath)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | format, err := formatter.Format(filepath)
29 | if err != nil {
30 | return err
31 | }
32 |
33 | err = os.WriteFile(filepath, []byte(format), fileInfo.Mode())
34 | if err != nil {
35 | return err
36 | }
37 | }
38 |
39 | return nil
40 | }
41 |
--------------------------------------------------------------------------------
/cmd/brewkit/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 |
6 | "github.com/urfave/cli/v2"
7 |
8 | appversion "github.com/ispringtech/brewkit/internal/frontend/app/version"
9 | )
10 |
11 | func version() *cli.Command {
12 | return &cli.Command{
13 | Name: "version",
14 | Usage: "Show brewkit version info",
15 | Action: executeVersion,
16 | }
17 | }
18 |
19 | func executeVersion(ctx *cli.Context) error {
20 | var opt commonOpt
21 | opt.scan(ctx)
22 |
23 | logger := makeLogger(opt.verbose)
24 |
25 | v := struct {
26 | APIVersion string `json:"apiVersion"`
27 | Commit string `json:"commit"`
28 | Dockerfile string `json:"dockerfile"`
29 | }{
30 | APIVersion: appversion.APIVersionV1,
31 | Commit: Commit,
32 | Dockerfile: DockerfileImage,
33 | }
34 |
35 | bytes, err := json.Marshal(v)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | logger.Outputf(string(bytes) + "\n")
41 |
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/internal/common/infrastructure/executor/opt.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type options struct {
8 | env []string
9 | logger Logger
10 | }
11 |
12 | type Opt interface {
13 | apply(*options)
14 | }
15 |
16 | type optFunc func(*options)
17 |
18 | func (f optFunc) apply(o *options) {
19 | f(o)
20 | }
21 |
22 | func WithEnv(env []string) Opt {
23 | return optFunc(func(o *options) {
24 | o.env = append(o.env, env...)
25 | })
26 | }
27 |
28 | func WithEnvMap(envMap EnvMap) Opt {
29 | return optFunc(func(o *options) {
30 | o.env = append(o.env, envMap.slice()...)
31 | })
32 | }
33 |
34 | func WithLogger(logger Logger) Opt {
35 | return optFunc(func(o *options) {
36 | o.logger = logger
37 | })
38 | }
39 |
40 | type EnvMap map[string]string
41 |
42 | func (e EnvMap) slice() []string {
43 | res := make([]string, 0, len(e))
44 | for k, v := range e {
45 | res = append(res, fmt.Sprintf("%s=%s", k, v))
46 | }
47 | return res
48 | }
49 |
--------------------------------------------------------------------------------
/internal/common/infrastructure/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | )
7 |
8 | type Logger interface {
9 | Logf(format string, a ...any)
10 | Outputf(format string, a ...any) // Used to output info to user
11 | Debugf(format string, a ...any)
12 | }
13 |
14 | func NewLogger(outputWriter, logWriter io.Writer, debug bool) Logger {
15 | return &logger{
16 | outputWriter: outputWriter,
17 | logWriter: logWriter,
18 | debug: debug,
19 | }
20 | }
21 |
22 | type logger struct {
23 | outputWriter io.Writer
24 | logWriter io.Writer
25 | debug bool
26 | }
27 |
28 | func (l *logger) Logf(format string, a ...any) {
29 | _, _ = fmt.Fprintf(l.logWriter, format, a...)
30 | }
31 |
32 | func (l *logger) Outputf(format string, a ...any) {
33 | _, _ = fmt.Fprintf(l.outputWriter, format, a...)
34 | }
35 |
36 | func (l *logger) Debugf(format string, a ...any) {
37 | if l.debug {
38 | _, _ = fmt.Fprintf(l.logWriter, format, a...)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | deadline: 5m
3 | issues-exit-code: 1
4 |
5 | linters:
6 | enable:
7 | - errcheck
8 | - gosimple
9 | - govet
10 | - ineffassign
11 | - staticcheck
12 | - unused
13 | - typecheck
14 | - unused
15 | - bodyclose
16 | - importas
17 | - dogsled
18 | - dupl
19 | - gochecknoinits
20 | - gocognit
21 | - gocritic
22 | - gocyclo
23 | - gofmt
24 | - goimports
25 | - revive
26 | - gosec
27 | - misspell
28 | - nakedret
29 | - prealloc
30 | - exportloopref
31 | - stylecheck
32 | - unconvert
33 | - whitespace
34 | - rowserrcheck
35 | - goconst
36 | - asciicheck
37 | - nestif
38 | - exportloopref
39 | - sqlclosecheck
40 |
41 | linters-settings:
42 | govet:
43 | check-shadowing: true
44 | goimports:
45 | local-prefixes: brewkit
46 | gocritic:
47 | disabled-checks:
48 | - sloppyReassign
49 | - whyNoLint
50 | enabled-tags:
51 | - experimental
52 | - opinionated
53 | wrapcheck:
54 | ignorePackageGlobs:
55 | - brewkit/*
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Richmedia Ltd
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the “Software”), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/internal/frontend/app/builddefinition/trace.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | stdslices "golang.org/x/exp/slices"
8 |
9 | "github.com/ispringtech/brewkit/internal/common/slices"
10 | )
11 |
12 | const (
13 | from = "from"
14 | deps = "deps"
15 | copyDirective = "copy"
16 | )
17 |
18 | type traceEntry struct {
19 | name string
20 | directive string
21 | }
22 |
23 | type trace []traceEntry
24 |
25 | func (t *trace) push(s traceEntry) {
26 | *t = append(*t, s)
27 | }
28 |
29 | func (t *trace) pop() {
30 | *t = (*t)[:len(*t)-1]
31 | }
32 |
33 | func (t *trace) has(s string) bool {
34 | return stdslices.ContainsFunc(*t, func(entry traceEntry) bool {
35 | return entry.name == s
36 | })
37 | }
38 |
39 | func (t *trace) String() string {
40 | if len(*t) == 0 {
41 | return ""
42 | }
43 |
44 | res := make([]traceEntry, 0, len(*t))
45 | for i := len(*t) - 1; i >= 0; i-- {
46 | res = append(res, (*t)[i])
47 | }
48 |
49 | return strings.Join(slices.Map(res, func(e traceEntry) string {
50 | return fmt.Sprintf("%s(%s)", e.name, e.directive)
51 | }), "->")
52 | }
53 |
--------------------------------------------------------------------------------
/internal/backend/app/docker/client.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/ispringtech/brewkit/internal/common/maybe"
7 | "github.com/ispringtech/brewkit/internal/dockerfile"
8 | )
9 |
10 | type BuildParams struct {
11 | Target string
12 | SSHAgent maybe.Maybe[string]
13 | Secrets []SecretData
14 | Output maybe.Maybe[string]
15 | }
16 |
17 | type ValueParams struct {
18 | Var string
19 | SSHAgent maybe.Maybe[string]
20 | Secrets []SecretData
21 | UseCache bool
22 | }
23 |
24 | type ClearCacheParams struct {
25 | All bool
26 | }
27 |
28 | type SecretData struct {
29 | ID string
30 | Path string
31 | }
32 |
33 | type Image struct {
34 | Img string // Image with repository and tag
35 | }
36 |
37 | type Client interface {
38 | Build(ctx context.Context, dockerfile dockerfile.Dockerfile, params BuildParams) error
39 | Value(ctx context.Context, dockerfile dockerfile.Dockerfile, params ValueParams) ([]byte, error)
40 | PullImage(ctx context.Context, img string) error
41 | ListImages(ctx context.Context, images []string) ([]Image, error)
42 | BuildImage(ctx context.Context, dockerfilePath string) error
43 |
44 | ClearCache(ctx context.Context, params ClearCacheParams) error
45 | }
46 |
--------------------------------------------------------------------------------
/internal/frontend/app/buildconfig/config.go:
--------------------------------------------------------------------------------
1 | package buildconfig
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/maybe"
5 | )
6 |
7 | type Config struct {
8 | APIVersion string
9 | Vars []VarData
10 | Targets []TargetData
11 | }
12 |
13 | type VarData struct {
14 | Name string
15 | From string
16 | Platform maybe.Maybe[string]
17 | WorkDir string
18 | Env map[string]string
19 | Cache []Cache
20 | Copy []Copy
21 | Secrets []Secret
22 | Network maybe.Maybe[string]
23 | SSH maybe.Maybe[SSH]
24 | Command string
25 | }
26 |
27 | type TargetData struct {
28 | Name string
29 | DependsOn []string
30 | Stage maybe.Maybe[StageData]
31 | }
32 |
33 | type StageData struct {
34 | From string
35 | Env map[string]string
36 | Command maybe.Maybe[string]
37 | SSH maybe.Maybe[SSH]
38 | Cache []Cache
39 | Copy []Copy
40 | Secrets []Secret
41 | Platform maybe.Maybe[string]
42 | WorkDir string
43 | Network maybe.Maybe[string]
44 | Output maybe.Maybe[Output]
45 | }
46 |
47 | type SSH struct{}
48 |
49 | type Cache struct {
50 | ID string
51 | Path string
52 | }
53 |
54 | type Copy struct {
55 | From maybe.Maybe[string]
56 | Src string
57 | Dst string
58 | }
59 |
60 | type Secret struct {
61 | ID string
62 | Path string
63 | }
64 |
65 | type Output struct {
66 | Artifact string
67 | Local string
68 | }
69 |
--------------------------------------------------------------------------------
/cmd/brewkit/cache.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/urfave/cli/v2"
5 |
6 | backendcache "github.com/ispringtech/brewkit/internal/backend/app/cache"
7 | "github.com/ispringtech/brewkit/internal/backend/infrastructure/docker"
8 | "github.com/ispringtech/brewkit/internal/frontend/app/service"
9 | )
10 |
11 | func cache() *cli.Command {
12 | return &cli.Command{
13 | Name: "cache",
14 | Usage: "Manipulate brewkit docker cache",
15 | Subcommands: []*cli.Command{
16 | cacheClear(),
17 | },
18 | }
19 | }
20 |
21 | func cacheClear() *cli.Command {
22 | return &cli.Command{
23 | Name: "clear",
24 | Usage: "Clear docker builder cache",
25 | Flags: []cli.Flag{
26 | &cli.BoolFlag{
27 | Name: "all",
28 | Aliases: []string{"a"},
29 | Usage: "Clear all cache, not just dangling ones",
30 | },
31 | },
32 | Action: func(ctx *cli.Context) error {
33 | var opts commonOpt
34 | opts.scan(ctx)
35 | clearAll := ctx.Bool("all")
36 |
37 | logger := makeLogger(opts.verbose)
38 |
39 | dockerClient, err := docker.NewClient(opts.dockerClientConfigPath, logger)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | cacheAPI := backendcache.NewCacheService(dockerClient)
45 | cacheService := service.NewCacheService(cacheAPI)
46 |
47 | return cacheService.ClearCache(ctx.Context, service.ClearCacheParam{
48 | All: clearAll,
49 | })
50 | },
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/cmd/brewkit/common.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/pkg/errors"
7 | "github.com/urfave/cli/v2"
8 |
9 | "github.com/ispringtech/brewkit/internal/common/infrastructure/logger"
10 | "github.com/ispringtech/brewkit/internal/common/maybe"
11 | appconfig "github.com/ispringtech/brewkit/internal/frontend/app/config"
12 | infraconfig "github.com/ispringtech/brewkit/internal/frontend/infrastructure/config"
13 | )
14 |
15 | type commonOpt struct {
16 | configPath string
17 | verbose bool
18 | dockerClientConfigPath maybe.Maybe[string]
19 | }
20 |
21 | func (o *commonOpt) scan(ctx *cli.Context) {
22 | o.configPath = ctx.String("config")
23 | o.verbose = ctx.Bool("verbose")
24 | dockerConfigPath := ctx.String("docker-config")
25 | if dockerConfigPath != "" {
26 | o.dockerClientConfigPath = maybe.NewJust(dockerConfigPath)
27 | }
28 | }
29 |
30 | func makeLogger(verbose bool) logger.Logger {
31 | return logger.NewLogger(os.Stdout, os.Stderr, verbose)
32 | }
33 |
34 | func parseConfig(configPath string, log logger.Logger) (appconfig.Config, error) {
35 | c, err := infraconfig.Parser{}.Config(configPath)
36 | if err != nil {
37 | if !errors.Is(err, appconfig.ErrConfigNotFound) {
38 | return appconfig.Config{}, err
39 | }
40 | log.Debugf("config not found in %s: default config will be used\n", configPath)
41 |
42 | return appconfig.DefaultConfig, nil
43 | }
44 | return c, err
45 | }
46 |
--------------------------------------------------------------------------------
/internal/common/either/either.go:
--------------------------------------------------------------------------------
1 | package either
2 |
3 | type discriminator uint
4 |
5 | const (
6 | left discriminator = iota
7 | right
8 | )
9 |
10 | func NewLeft[L any, R any](l L) Either[L, R] {
11 | return Either[L, R]{
12 | l: l,
13 | d: left,
14 | }
15 | }
16 |
17 | func NewEitherLeft[T Either[L, R], L any, R any](l L) T {
18 | return T(Either[L, R]{
19 | l: l,
20 | d: left,
21 | })
22 | }
23 |
24 | func NewRight[L any, R any](r R) Either[L, R] {
25 | return Either[L, R]{
26 | r: r,
27 | d: right,
28 | }
29 | }
30 |
31 | func NewEitherRight[T Either[L, R], L any, R any](r R) T {
32 | return T(Either[L, R]{
33 | r: r,
34 | d: right,
35 | })
36 | }
37 |
38 | type Either[L any, R any] struct {
39 | l L
40 | r R
41 | d discriminator
42 | }
43 |
44 | func (e Either[L, R]) MapLeft(f func(l L)) Either[L, R] {
45 | if e.d == left {
46 | f(e.l)
47 | }
48 | return e
49 | }
50 |
51 | func (e Either[L, R]) MapRight(f func(r R)) Either[L, R] {
52 | if e.d == right {
53 | f(e.r)
54 | }
55 | return e
56 | }
57 |
58 | func (e Either[T, E]) IsLeft() bool {
59 | return e.d == left
60 | }
61 |
62 | func (e Either[T, E]) IsRight() bool {
63 | return e.d == right
64 | }
65 |
66 | func (e Either[T, E]) Left() T {
67 | if e.d != left {
68 | panic("violated usage of either")
69 | }
70 | return e.l
71 | }
72 |
73 | func (e Either[T, E]) Right() E {
74 | if e.d != right {
75 | panic("violated usage of either")
76 | }
77 | return e.r
78 | }
79 |
--------------------------------------------------------------------------------
/internal/frontend/infrastructure/builddefinition/funcs.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | var funcs = []nativeFunc{
4 | nativeFunc2[string, string]{
5 | name: "cache",
6 | v1: argDesc{
7 | name: "id",
8 | },
9 | v2: argDesc{
10 | name: "path",
11 | },
12 | f: func(id string, path string) (interface{}, error) {
13 | return map[string]interface{}{
14 | "id": id,
15 | "path": path,
16 | }, nil
17 | },
18 | },
19 | nativeFunc2[string, string]{
20 | name: "copy",
21 | v1: argDesc{
22 | name: "src",
23 | },
24 | v2: argDesc{
25 | name: "dst",
26 | },
27 | f: func(src string, dst string) (interface{}, error) {
28 | return map[string]interface{}{
29 | "src": src,
30 | "dst": dst,
31 | }, nil
32 | },
33 | },
34 | nativeFunc3[string, string, string]{
35 | name: "copyFrom",
36 | v1: argDesc{
37 | name: "from",
38 | },
39 | v2: argDesc{
40 | name: "src",
41 | },
42 | v3: argDesc{
43 | name: "dst",
44 | },
45 | f: func(from string, src string, dst string) (interface{}, error) {
46 | return map[string]interface{}{
47 | "from": from,
48 | "src": src,
49 | "dst": dst,
50 | }, nil
51 | },
52 | },
53 | nativeFunc2[string, string]{
54 | name: "secret",
55 | v1: argDesc{
56 | name: "id",
57 | },
58 | v2: argDesc{
59 | name: "path",
60 | },
61 | f: func(id string, path string) (interface{}, error) {
62 | return map[string]interface{}{
63 | "id": id,
64 | "path": path,
65 | }, nil
66 | },
67 | },
68 | }
69 |
--------------------------------------------------------------------------------
/internal/common/maybe/maybe.go:
--------------------------------------------------------------------------------
1 | package maybe
2 |
3 | type Maybe[T any] struct {
4 | v T
5 | valid bool
6 | }
7 |
8 | func NewJust[T any](v T) Maybe[T] {
9 | return Maybe[T]{
10 | v: v,
11 | valid: true,
12 | }
13 | }
14 |
15 | // NewNone used for explicit none value
16 | func NewNone[T any]() Maybe[T] {
17 | return Maybe[T]{}
18 | }
19 |
20 | func Valid[T any](maybe Maybe[T]) bool {
21 | return maybe.valid
22 | }
23 |
24 | func Just[T any](maybe Maybe[T]) T {
25 | if !Valid(maybe) {
26 | panic("violated usage of maybe: Just on non Valid Maybe")
27 | }
28 | return maybe.v
29 | }
30 |
31 | // MapNone returns underlying value on Valid Maybe or value from f
32 | func MapNone[T any](m Maybe[T], f func() T) T {
33 | if !Valid(m) {
34 | return f()
35 | }
36 | return Just(m)
37 | }
38 |
39 | func FromPtr[T any](t *T) Maybe[T] {
40 | if t == nil {
41 | return NewNone[T]()
42 | }
43 | return NewJust[T](*t)
44 | }
45 |
46 | func ToPtr[T any](m Maybe[T]) *T {
47 | if m.valid {
48 | return &m.v
49 | }
50 | return nil
51 | }
52 |
53 | func Map[T any, E any](m Maybe[T], f func(T) E) Maybe[E] {
54 | if !Valid(m) {
55 | return Maybe[E]{}
56 | }
57 |
58 | return Maybe[E]{
59 | v: f(Just(m)),
60 | valid: true,
61 | }
62 | }
63 |
64 | func MapErr[T any, E any](m Maybe[T], f func(T) (E, error)) (Maybe[E], error) {
65 | if !Valid(m) {
66 | return Maybe[E]{}, nil
67 | }
68 |
69 | e, err := f(Just(m))
70 | if err != nil {
71 | return Maybe[E]{}, err
72 | }
73 |
74 | return Maybe[E]{
75 | v: e,
76 | valid: true,
77 | }, nil
78 | }
79 |
--------------------------------------------------------------------------------
/docs/cli/overview.md:
--------------------------------------------------------------------------------
1 | # CLI
2 |
3 | BrewKit has command-line-interface for running build, manipulate cache and config.
4 |
5 | You can learn more about each command by running command with `-h` flag
6 |
7 | ## build
8 |
9 | Manipulates builds
10 |
11 | | Command | Description |
12 | |------------------|---------------------------------------------------------------------------------------------|
13 | | | Runs specified target |
14 | | definition | Print full parsed and verified build-definition in JSON to stdout |
15 | | definition-debug | Print compiled build definition in raw JSON, useful for debugging complex build definitions |
16 |
17 | Examples:
18 |
19 | Build concrete targets
20 | ```shell
21 | brewkit build generate compile
22 | ```
23 |
24 | ## config
25 |
26 | Manipulate host config
27 |
28 | | Command | Description |
29 | |---------|-------------------------------|
30 | | init | Create default brewkit config |
31 |
32 | ## version
33 |
34 | Print BrewKit version
35 |
36 | ```shell
37 | brewkit version
38 | ```
39 |
40 | ## cache
41 |
42 | Manipulate buildkit cache
43 |
44 | | Command | Description |
45 | |----------|-----------------------------------------|
46 | | clear | Clear docker builder cache |
47 | | clear -a | Clear all cache, not just dangling ones |
48 |
49 | ## fmt
50 |
51 | Pretty format jsonnet files with jsonnetfmt
52 |
53 | ```shell
54 | brewkit fmt brewkit.jsonnet
55 | ```
--------------------------------------------------------------------------------
/cmd/brewkit/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "os"
7 | "path"
8 |
9 | "github.com/pkg/errors"
10 | "github.com/urfave/cli/v2"
11 |
12 | appconfig "github.com/ispringtech/brewkit/internal/frontend/app/config"
13 | infraconfig "github.com/ispringtech/brewkit/internal/frontend/infrastructure/config"
14 | )
15 |
16 | func config() *cli.Command {
17 | return &cli.Command{
18 | Name: "config",
19 | Usage: "Manipulate brewkit config",
20 | Subcommands: []*cli.Command{
21 | configInit(),
22 | },
23 | }
24 | }
25 |
26 | func configInit() *cli.Command {
27 | return &cli.Command{
28 | Name: "init",
29 | Usage: "Create default brewkit config",
30 | Action: func(ctx *cli.Context) error {
31 | var opts commonOpt
32 | opts.scan(ctx)
33 |
34 | logger := makeLogger(opts.verbose)
35 |
36 | defaultConfig, err := infraconfig.Parser{}.Dump(appconfig.DefaultConfig)
37 | if err != nil {
38 | return err
39 | }
40 |
41 | defaultConfigBuffer := &bytes.Buffer{}
42 | err = json.Indent(defaultConfigBuffer, defaultConfig, "", " ")
43 | if err != nil {
44 | return err
45 | }
46 |
47 | configPath := opts.configPath
48 | configDir := path.Dir(configPath)
49 |
50 | err = os.MkdirAll(configDir, 0o755)
51 | if err != nil {
52 | return errors.Wrapf(err, "failed to create folder for config %s", configDir)
53 | }
54 |
55 | err = os.WriteFile(configPath, defaultConfigBuffer.Bytes(), 0o600)
56 | if err != nil {
57 | return errors.Wrapf(err, "failed to write file for config %s", configDir)
58 | }
59 |
60 | logger.Outputf("Default config created in %s\n", configPath)
61 |
62 | return nil
63 | },
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/internal/frontend/infrastructure/config/parser.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "os"
6 | "path"
7 |
8 | "github.com/google/go-jsonnet"
9 | "github.com/pkg/errors"
10 |
11 | "github.com/ispringtech/brewkit/internal/common/slices"
12 | "github.com/ispringtech/brewkit/internal/frontend/app/config"
13 | )
14 |
15 | type Parser struct{}
16 |
17 | func (p Parser) Config(configPath string) (config.Config, error) {
18 | fileBytes, err := os.ReadFile(configPath)
19 | if err != nil {
20 | if errors.Is(err, os.ErrNotExist) {
21 | return config.Config{}, errors.WithStack(config.ErrConfigNotFound)
22 | }
23 |
24 | return config.Config{}, errors.Wrap(err, "failed to read config file")
25 | }
26 |
27 | vm := jsonnet.MakeVM()
28 |
29 | jsonnet.Version()
30 |
31 | data, err := vm.EvaluateAnonymousSnippet(path.Base(configPath), string(fileBytes))
32 | if err != nil {
33 | return config.Config{}, errors.Wrap(err, "failed to compile jsonnet for config")
34 | }
35 |
36 | var c Config
37 |
38 | err = json.Unmarshal([]byte(data), &c)
39 | if err != nil {
40 | return config.Config{}, errors.Wrap(err, "failed to parse json config")
41 | }
42 |
43 | return config.Config{
44 | Secrets: slices.Map(c.Secrets, func(s Secret) config.Secret {
45 | return config.Secret{
46 | ID: s.ID,
47 | Path: os.ExpandEnv(s.Path),
48 | }
49 | }),
50 | }, nil
51 | }
52 |
53 | func (p Parser) Dump(srcConfig config.Config) ([]byte, error) {
54 | c := Config{
55 | Secrets: slices.Map(srcConfig.Secrets, func(s config.Secret) Secret {
56 | return Secret{
57 | ID: s.ID,
58 | Path: s.Path,
59 | }
60 | }),
61 | }
62 |
63 | data, err := json.Marshal(c)
64 | return data, errors.Wrap(err, "failed to marshal config to json")
65 | }
66 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
2 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
3 | github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g=
4 | github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA=
5 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
6 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
7 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
8 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
9 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
10 | github.com/urfave/cli/v2 v2.25.1 h1:zw8dSP7ghX0Gmm8vugrs6q9Ku0wzweqPyshy+syu9Gw=
11 | github.com/urfave/cli/v2 v2.25.1/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
12 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
13 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
14 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb h1:rhjz/8Mbfa8xROFiH+MQphmAmgqRM0bOMnytznhWEXk=
15 | golang.org/x/exp v0.0.0-20230420155640-133eef4313cb/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
18 | gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
19 | gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
20 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
21 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
22 |
--------------------------------------------------------------------------------
/docs/build-definition/jsonnet-extensions.md:
--------------------------------------------------------------------------------
1 | # Jsonnet extensions
2 |
3 | ## cache
4 |
5 | Allows to write cache definition as one-liner
6 |
7 | See [build-definition reference](reference.md#cache)
8 |
9 | ```jsonnet
10 | local cache = std.native('cache');
11 | //
12 | targets: {
13 | gobuild: {
14 | // ...
15 | cache: cache("go-build", "/app/cache"),
16 | // ...
17 | }
18 | }
19 | //
20 | ```
21 |
22 | ## copy
23 |
24 | Allows to write copy definition as one-liner
25 |
26 | See [build-definition reference](reference.md#copy)
27 |
28 | ```jsonnet
29 | local copy = std.native('copy');
30 | //
31 | targets: {
32 | gobuild: {
33 | // ...
34 | copy: [
35 | copy('cmd', 'cmd'),
36 | copy('pkg', 'pkg'),
37 | ],
38 | // ...
39 | }
40 | }
41 | //
42 | ```
43 |
44 | ## copyFrom
45 |
46 | Allows to write copyFrom definition as one-liner
47 |
48 | See [build-definition reference](reference.md#copy)
49 |
50 | ```jsonnet
51 | local copyFrom = std.native('copyFrom');
52 | //
53 | targets: {
54 | gobuild: {
55 | // ...
56 | copy: [
57 | copyFrom('prebuild', 'artifacts', 'artifacts'),
58 | copy('cmd', 'cmd'),
59 | copy('pkg', 'pkg'),
60 | ],
61 | // ...
62 | }
63 | }
64 | //
65 | ```
66 |
67 | ## secret
68 |
69 | Allows to write secret definition as one-liner
70 |
71 | See [build-definition reference](reference.md#secrets)
72 |
73 | ```jsonnet
74 | local secret = std.native('secret');
75 | //
76 | targets: {
77 | gobuild: {
78 | // ...
79 | secret: secret("aws", '/root/.aws/credentials'),
80 | // ...
81 | }
82 | }
83 | //
84 | ```
--------------------------------------------------------------------------------
/internal/backend/api/model.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/either"
5 | "github.com/ispringtech/brewkit/internal/common/maybe"
6 | )
7 |
8 | type Vertex struct {
9 | Name string // Unique Vertex name
10 |
11 | Stage maybe.Maybe[Stage]
12 |
13 | From maybe.Maybe[*Vertex] // Parent vertex
14 | DependsOn []Vertex
15 | }
16 |
17 | type Stage struct {
18 | From string // From for stage
19 | Platform maybe.Maybe[string] // Platform for image
20 | WorkDir string // Working directory for stage
21 | Env map[string]string // Stage env
22 | Cache []Cache // Pluggable cache for build systems
23 | Copy []Copy // Copy local or build stages artifacts
24 | Network maybe.Maybe[Network] // Network options
25 | SSH maybe.Maybe[SSH] // SSH access options
26 | Secrets []Secret
27 | Command maybe.Maybe[string] // Command for stage
28 | Output maybe.Maybe[Output] // Output artifacts from builder
29 | }
30 |
31 | type Image struct {
32 | From string
33 | Dockerfile string
34 | }
35 |
36 | type Var struct {
37 | Name string // Unique Var name
38 | From string
39 | Platform maybe.Maybe[string]
40 | WorkDir string
41 | Env map[string]string
42 | Cache []Cache
43 | Copy []CopyVar
44 | Secrets []Secret
45 | Network maybe.Maybe[Network]
46 | SSH maybe.Maybe[SSH]
47 | Command string
48 | }
49 |
50 | type Copy struct {
51 | From maybe.Maybe[either.Either[*Vertex, string]]
52 | Src string
53 | Dst string
54 | }
55 |
56 | // CopyVar is Copy instruction for var
57 | type CopyVar struct {
58 | From maybe.Maybe[string]
59 | Src string
60 | Dst string
61 | }
62 |
63 | type Cache struct {
64 | ID string
65 | Path string
66 | }
67 |
68 | type Secret struct {
69 | ID string
70 | MountPath string
71 | }
72 |
73 | type SecretSrc struct {
74 | ID string
75 | SourcePath string
76 | }
77 |
78 | type SSH struct{}
79 |
80 | type Network struct {
81 | Network string // It may be Host and other docker networks
82 | }
83 |
84 | type Output struct {
85 | Artifact string
86 | Local string
87 | }
88 |
--------------------------------------------------------------------------------
/internal/frontend/app/builddefinition/builder.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | import (
4 | "github.com/pkg/errors"
5 |
6 | "github.com/ispringtech/brewkit/internal/backend/api"
7 | "github.com/ispringtech/brewkit/internal/common/maybe"
8 | "github.com/ispringtech/brewkit/internal/common/slices"
9 | "github.com/ispringtech/brewkit/internal/frontend/app/buildconfig"
10 | "github.com/ispringtech/brewkit/internal/frontend/app/config"
11 | "github.com/ispringtech/brewkit/internal/frontend/app/version"
12 | )
13 |
14 | type Builder interface {
15 | Build(config buildconfig.Config, secrets []config.Secret) (Definition, error)
16 | }
17 |
18 | func NewBuilder() Builder {
19 | return &builder{}
20 | }
21 |
22 | type builder struct{}
23 |
24 | func (builder builder) Build(c buildconfig.Config, secrets []config.Secret) (Definition, error) {
25 | if c.APIVersion != version.APIVersionV1 {
26 | return Definition{}, errors.Wrapf(ErrUnsupportedAPIVersion, "version: %s", c.APIVersion)
27 | }
28 |
29 | vertexes, err := newVertexGraphBuilder(secrets, c.Targets).graphVertexes()
30 | if err != nil {
31 | return Definition{}, err
32 | }
33 |
34 | vars, err := builder.variables(c.Vars, secrets)
35 | if err != nil {
36 | return Definition{}, err
37 | }
38 |
39 | return Definition{
40 | Vertexes: vertexes,
41 | Vars: vars,
42 | }, err
43 | }
44 |
45 | func (builder builder) variables(vars []buildconfig.VarData, secrets []config.Secret) ([]api.Var, error) {
46 | return slices.MapErr(vars, func(v buildconfig.VarData) (api.Var, error) {
47 | mappedSecrets, err := mapSecrets(v.Secrets, secrets)
48 | if err != nil {
49 | return api.Var{}, errors.Wrapf(err, "failed to map secrets in %s variable", v.Name)
50 | }
51 |
52 | return api.Var{
53 | Name: v.Name,
54 | From: v.From,
55 | Platform: maybe.Map(v.Platform, func(p string) string {
56 | return p
57 | }),
58 | WorkDir: v.WorkDir,
59 | Env: v.Env,
60 | Cache: slices.Map(v.Cache, mapCache),
61 | Copy: slices.Map(v.Copy, mapCopy),
62 | Network: maybe.Map(v.Network, func(n string) api.Network {
63 | return api.Network{
64 | Network: n,
65 | }
66 | }),
67 | SSH: maybe.Map(v.SSH, func(s buildconfig.SSH) api.SSH {
68 | return api.SSH{}
69 | }),
70 | Secrets: mappedSecrets,
71 | Command: v.Command,
72 | }, nil
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/internal/common/slices/map.go:
--------------------------------------------------------------------------------
1 | package slices
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/maybe"
5 | )
6 |
7 | // Map iterates through slice and maps values
8 | func Map[T, TResult any](s []T, f func(T) TResult) []TResult {
9 | result := make([]TResult, 0, len(s))
10 | for _, t := range s {
11 | result = append(result, f(t))
12 | }
13 | return result
14 | }
15 |
16 | // MapErr iterates through slice and maps values and stops on any error
17 | func MapErr[T, TResult any](s []T, f func(T) (TResult, error)) ([]TResult, error) {
18 | result := make([]TResult, 0, len(s))
19 | for _, t := range s {
20 | e, err := f(t)
21 | if err != nil {
22 | return nil, err
23 | }
24 | result = append(result, e)
25 | }
26 | return result, nil
27 | }
28 |
29 | // Filter iterates and adds to result slice elements that satisfied predicate
30 | func Filter[T any](s []T, f func(T) bool) []T {
31 | var result []T
32 | for _, t := range s {
33 | if f(t) {
34 | result = append(result, t)
35 | }
36 | }
37 | return result
38 | }
39 |
40 | // FilterErr iterates and adds to result slice elements that satisfied predicate and stop on any error
41 | func FilterErr[T any](s []T, f func(T) (bool, error)) ([]T, error) {
42 | var result []T
43 | for _, t := range s {
44 | accepted, err := f(t)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | if accepted {
50 | result = append(result, t)
51 | }
52 | }
53 |
54 | return result, nil
55 | }
56 |
57 | func MapMaybe[T, TResult any](s []T, f func(T) maybe.Maybe[TResult]) (res []TResult) {
58 | for _, t := range s {
59 | m := f(t)
60 | if maybe.Valid(m) {
61 | res = append(res, maybe.Just(m))
62 | }
63 | }
64 | return res
65 | }
66 |
67 | func MapMaybeErr[T, TResult any](s []T, f func(T) (maybe.Maybe[TResult], error)) (res []TResult, err error) {
68 | for _, t := range s {
69 | var m maybe.Maybe[TResult]
70 | m, err = f(t)
71 | if err != nil {
72 | return nil, err
73 | }
74 | if maybe.Valid(m) {
75 | res = append(res, maybe.Just(m))
76 | }
77 | }
78 | return res, nil
79 | }
80 |
81 | // Merge slices into one
82 | func Merge[T any](slices ...[]T) []T {
83 | if len(slices) == 0 {
84 | return nil
85 | }
86 |
87 | result := slices[0]
88 | for i := 1; i < len(slices); i++ {
89 | s := slices[i]
90 |
91 | result = append(result, s...)
92 | }
93 |
94 | return result
95 | }
96 |
--------------------------------------------------------------------------------
/internal/frontend/infrastructure/builddefinition/model.go:
--------------------------------------------------------------------------------
1 | package builddefinition
2 |
3 | import (
4 | "github.com/ispringtech/brewkit/internal/common/either"
5 | "github.com/ispringtech/brewkit/internal/common/maybe"
6 | )
7 |
8 | type Config struct {
9 | APIVersion string `json:"apiVersion"`
10 | Targets map[string]either.Either[[]string, Target] `json:"targets"`
11 | Vars map[string]Var `json:"vars"`
12 | }
13 |
14 | type Target struct {
15 | DependsOn []string `json:"dependsOn"`
16 | *Stage `json:",inline"`
17 | }
18 |
19 | type Stage struct {
20 | From string `json:"from"`
21 | Env map[string]string `json:"env"`
22 | SSH maybe.Maybe[SSH] `json:"ssh"`
23 | Cache []Cache `json:"cache"`
24 | Copy either.Either[[]Copy, Copy] `json:"copy"`
25 | Secrets either.Either[[]Secret, Secret] `json:"secret"`
26 | Platform maybe.Maybe[string] `json:"platform"`
27 | WorkDir string `json:"workdir"`
28 | Network maybe.Maybe[string] `json:"network"`
29 | Command maybe.Maybe[string] `json:"command"`
30 | Output maybe.Maybe[Output] `json:"output"`
31 | }
32 |
33 | type Var struct {
34 | From string `json:"from"`
35 | Platform maybe.Maybe[string] `json:"platform"`
36 | WorkDir string `json:"workdir"`
37 | Env map[string]string `json:"env"`
38 | Cache []Cache `json:"cache"`
39 | Copy either.Either[[]Copy, Copy] `json:"copy"`
40 | Secrets either.Either[[]Secret, Secret] `json:"secrets"`
41 | Network maybe.Maybe[string] `json:"network"`
42 | SSH maybe.Maybe[SSH] `json:"ssh"`
43 | Command string `json:"command"`
44 | }
45 |
46 | type Cache struct {
47 | ID string `yaml:"id"`
48 | Path string `yaml:"path"`
49 | }
50 |
51 | type Copy struct {
52 | From maybe.Maybe[string] `json:"from"`
53 | Src string `json:"src"`
54 | Dst string `json:"dst"`
55 | }
56 |
57 | type SSH struct{}
58 |
59 | type Secret struct {
60 | ID string `yaml:"id"`
61 | Path string `yaml:"path"`
62 | }
63 |
64 | type Output struct {
65 | Artifact string `json:"artifact"`
66 | Local string `json:"local"`
67 | }
68 |
--------------------------------------------------------------------------------
/docs/diagrams/architecture.drawio.xml:
--------------------------------------------------------------------------------
1 |
2 | 5VjbUtswEP0az9AHGMcmIXnMBdpOactMphceFXuxVWQrI69Jwtd3FcnxLUAayKSdvoB0vFrLZ8/uSnH8cbJ8r9g8/ixDEI7nhkvHnzie1/Vd+quBlQG8wYUBIsVDA3VKYMofwYJ2XZTzELKaIUopkM/rYCDTFAKsYUwpuaib3UlRf+ucRdACpgETbfQHDzE2aL/rlvgH4FFcvLnj2icJK4wtkMUslIsK5F86/lhJiWaULMcgNHcFL2bd1RNPNxtTkOIuCwb5l1O5HEdfe/zjENj0Pv3+cGq9PDCR2w+eyOAelOP1BHkdzfQo0qOTUc5F+InjO/s5uCo4ijERNOqQ1SLmCNM5C/SjBSmCMPsKUAjLJ/fe2TBCSgKZAKoVmdgF531LolVRpyB1UcakU0gmrsSjiBOzMog2rkumaGDJ+hPi3BYNEJJy7FQqjGUkUyYuS3SkZJ6GoN26NCttrqWcWwJ/AeLKpgHLURJUode8U7/oeSJpXzJXATzzAZ5NJqYiwJcU0g6MAsGQP9T38eYsey15jhQs7jkeQYObkvTXaLB/DAkSW2r1U68/6xbT2+qzydI6N7OVnb2hdP0dpeu9UrrrpUOl2KpiMJc8xazi+UYDFZn0ujWZeOduI9DGYxn2zdb2V4LfypNvmS7iDXlQC5rrYZ6IYYBSUVh0SnBqd9dsBuJGZhy5TMlkJhFlUjEYCh7pB6h10q5Jr0otv1HeN6lWSS1/S2b1DpVZg3bd0d2vRSh9MtbZYJamgD4d1Bb+Eh6GJhMh449stnalE8QKi/x2R053on1R8mUmD7XrDJW8h7EUOnCTVKbayx0XogG9Ra0bvByQ7paA+Adrt+2DyjoiJ46uws2zCv1niZZpOstM9XebkDnmEHvgeOO9PNCJcx3+PZfP9PZPqYTlCawLStNH+5z1tNoE3OG/qrVm8ve2tFVvi9YGB9Oad5S+uuRo2qrXtdPbopHSuOyqelI01Uozvqh147L/Hrwb73qQPE439vs1cfUbd6Sm+cB/jXlx/Dtor++0m/1UCiLQc2emSdFVmq7i7YpyBRjEdJmmoEKgQPO2pXbBGZ4F/0+ncxvHtR2rzx5HD5qWt32jhvInE//yNw==7Vffb5swEP5rkLaHSQRClD6GtF26tdKkatqPNwdfwIvhmDEh6V+/M5gQQhs1Vaf1YS8EPt+dfd99PjuOP0+3HxXLkzvkIB3P5VvHv3Q8LxhN6GmAXQN4wbQBYiV4A4064F48gAVdi5aCQ9Ez1IhSi7wPRphlEOkexpTCqm+2QtmfNWcxDID7iMkh+k1wnTToNHA7fAEiTtqZR64dSVlrbIEiYRyrA8i/cvy5QtTNW7qdgzTctbw0ftdPjO4XpiDTz3G4u11fBJPNzzhZ5DBlmsc3tx98uza9axMGTvnbT1Q6wRgzJq86NFRYZhxMVJe+OptbxJzAEYG/QOudLSYrNRKU6FTa0WZOM9GTqViowFJFcGL9rSSYikGfsPP2hJNQAVPQakd+CiTTYtNfB7OSifd2Hav0Yok9g2Qbd8NkaWe6VphpyPiA/QOWqkRouM9ZnX9Fm2vP3AaUhu1p7oa5WgdvbPXYbshWsFUn71G7+5IDabeSf3V6vAE9IYvW/4ad8fStsTMesON4E0mzhiX5TmJdJ94gUrTI1wKUiUwqUyzSArPWhhbRmQ0dQwXVZ6FNdljoprGuRFwqdkaQOZOyIN9lW0d39uXmpC+Bh+kc1Z3qqfsthEkRZ/QuYWVGTM0FNe2ZhbXpRJRKIR7Yso5iWlWOxEddnyB0gksThnpT0bQpE7XQCtcwR4mKkAwz0+5WQsoj6DW2oXcktGAotL34DoU2/VtCC14kNPr9XWJddFRA/riiR1gKacpe7AoNaWfzTP1cIulGvVuaKGuh379IyQtWtMJzOaxEBry+AJh9sTxe339ldi1w8gxljv+SMnFxMU+z3Y/QjT+F32cpTzbVI+en6VLrukvVuqDLR10WVUa6rFXIxUbwuuBkgOaGUGHdzNKciKq5np1RzAjMNI+UMxWcN5eiN1zR4yN/5A8r6j92qPnnV9Qxd5v2SluPHfwv8K/+AA==
--------------------------------------------------------------------------------
/cmd/brewkit/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | stdlog "log"
6 | "os"
7 | "os/signal"
8 | "syscall"
9 |
10 | "github.com/urfave/cli/v2"
11 |
12 | "github.com/ispringtech/brewkit/internal/dockerfile"
13 | appconfig "github.com/ispringtech/brewkit/internal/frontend/app/config"
14 | )
15 |
16 | const (
17 | appID = "brewkit"
18 | )
19 |
20 | // These variables come from -ldflags settings
21 | // Here also setup their fallback values
22 | var (
23 | Commit = "UNKNOWN"
24 | DockerfileImage = string(dockerfile.Dockerfile14) // default image for dockerfile
25 | )
26 |
27 | func main() {
28 | ctx := context.Background()
29 |
30 | ctx = subscribeForKillSignals(ctx)
31 |
32 | err := runApp(ctx, os.Args)
33 | if err != nil {
34 | stdlog.Fatal(err)
35 | }
36 | }
37 |
38 | func runApp(ctx context.Context, args []string) error {
39 | ctx, cancel := context.WithCancel(ctx)
40 | defer cancel()
41 |
42 | configPath, err := appconfig.DefaultConfigPath()
43 | if err != nil {
44 | configPath = "" // Ignore err if default path unavailable
45 | }
46 |
47 | workdir, err := os.Getwd()
48 | if err != nil {
49 | return err
50 | }
51 |
52 | app := &cli.App{
53 | Name: appID,
54 | Usage: "Container-native build system",
55 | Commands: []*cli.Command{
56 | build(workdir),
57 | config(),
58 | version(),
59 | cache(),
60 | fmtCommand(),
61 | },
62 | Flags: []cli.Flag{
63 | &cli.StringFlag{
64 | Name: "config",
65 | Usage: "brewkit config",
66 | Aliases: []string{"c"},
67 | EnvVars: []string{"BREWKIT_CONFIG"},
68 | Value: configPath,
69 | },
70 | &cli.BoolFlag{
71 | Name: "verbose",
72 | Usage: "Verbose output to stderr",
73 | Aliases: []string{"v"},
74 | },
75 | &cli.StringFlag{
76 | Name: "docker-config",
77 | Usage: "Path to docker client config",
78 | Aliases: []string{"dc"},
79 | EnvVars: []string{"BREWKIT_DOCKER_CONFIG"},
80 | },
81 | },
82 | }
83 |
84 | return app.RunContext(ctx, args)
85 | }
86 |
87 | func subscribeForKillSignals(ctx context.Context) context.Context {
88 | ctx, cancel := context.WithCancel(ctx)
89 |
90 | ch := make(chan os.Signal, 1)
91 | signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
92 |
93 | go func() {
94 | defer cancel()
95 | select {
96 | case <-ctx.Done():
97 | signal.Stop(ch)
98 | case <-ch:
99 | }
100 | }()
101 |
102 | return ctx
103 | }
104 |
--------------------------------------------------------------------------------
/docs/adr/001-buildkit.md:
--------------------------------------------------------------------------------
1 | # Using BuildKit as a build backend
2 |
3 | # Context and Problem Statement
4 |
5 | Using a direct container run(via `docker run...` or running docker containers) make brewkit highly dependent on docker daemon.
6 | We want to avoid this dependency, as BrewKit does not need to run containers
7 | It can only be used where there is a docker daemon. Although containers themselves as processes (as for kubernetes, for example) are not needed for Brewkit.
8 |
9 | # Decision Drivers
10 |
11 | - BrewKit should not depend on the docker daemon
12 | - When using BuildKit, you can get fancy features (caching, rootless artifacts, access to ssh-agent inside the builder) out of the box.
13 |
14 | # Considered Options
15 |
16 | - Using `docker run ...`
17 | - Using BuildKit
18 |
19 | # Decision Outcome
20 |
21 | The chosen variant is "Use BuildKit." Since it is a more modern way to build projects inside the container.
22 |
23 | # Pros and Cons of the Options
24 |
25 | ## Using `docker run ...`.
26 |
27 | All project build commands will run inside a container that is started via a direct request to the docker daemon. The container is started and a command is run inside it.
28 |
29 | * Good, simple solution
30 | * Bad, dependency on the Docker daemon
31 | * Bad, no caching depending on code changes in the project
32 |
33 | ## Using BuildKit
34 |
35 | Send build request to buildkit to build an image with specific instructions
36 |
37 | * Good, no root privileges needed
38 | * Good, already integrated in new versions of Docker to build images.
39 | * Bad, BuildKit not in final version yet (as of 05.2023)
40 | * Bad, BuildKit can't export build cache to another folder (relevant for Golang where package cache is not in project directory)
41 |
42 | ## More Information
43 |
44 | ## More information about BuildKit
45 |
46 | - [BuildKit github](https://github.com/moby/buildkit)
47 | - [Docker BuildKit documentation](https://docs.docker.com/build/buildkit/)
48 |
49 | ## Independence from the docker daemon
50 |
51 | `BuildKit` is a separate part of docker project used to build images
52 | `Buildkit` itself **does not require root privileges**, and can be shipped completely independent of `Docker`.
53 |
54 | Independence from `Docker` and root privileges provides the following benefits:
55 | - Easier and safer _Docker-in-Docker_: you don't need to pass a socket your docker into a container to build images inside the container, you can use a BuildKit image and build images through it
56 | - Running a container with BuildKit does not require `priveledged``: more secure image building in containers
--------------------------------------------------------------------------------
/docs/adr/002-jsonnet.md:
--------------------------------------------------------------------------------
1 | # Jsonnet - project build description language for BrewKit
2 |
3 | # Context and Problem Statement
4 |
5 | The project build description language for BrewKit should support a powerful templating mechanism, imports (for splitting into files) and have internal validation.
6 |
7 | # Decision Drivers
8 |
9 | - The configuration language must have a powerful templating mechanism
10 | - The language should either be easy to learn or be a mockup of one of the popular languages (YAML. JSON).
11 |
12 | # Considered Options
13 |
14 | - YAML
15 | - JSON
16 | - HCL
17 | - JSONNET
18 |
19 | # Decision Outcome
20 |
21 | Decision selected: "Use Jsonnet". Jsonnet offers great flexibility in templating Json documents and has a Go version that can be used as a library.
22 |
23 | # Pros and Cons of the Options
24 |
25 | ## YAML
26 |
27 | - Good, popular description format
28 | - Good, considered more human-friendly than JSON
29 | - Good, supports templating via anchors
30 | - Bad, has many problems related to its design (typing, nesting)
31 | - Bad, not enough templating tools to describe the build of a complex project
32 |
33 | ## JSON
34 |
35 | - Good, standard in describing schemas, configs, etc.
36 | - Bad, no templating => not suitable for describing a complex project assembly
37 |
38 | ## HCL - [github](https://github.com/hashicorp/hcl)
39 |
40 | A configuration description language from HashiCorp. Mostly used for [Terraform](https://www.terraform.io/) configuration.
41 |
42 | - Good, has built-in validation (you can set field optionality, etc.).
43 | - Good, is a new language for declarative style description
44 | - Bad, not enough templating tools to describe the build of a complex project (lack of variables, etc.).
45 |
46 | ## Jsonnet - [Jsonnet](https://jsonnet.org/)
47 |
48 | A superset of JSON augmented with functions, variables and object-oriented approach. Implemented by Google.
49 | Used for description in AML projects, actively used by [Grafana Labs](https://grafana.github.io/grafonnet-lib/).
50 |
51 | - Good, full-featured language with support for f-i, variables, etc.
52 | - Good, output is simple JSON that can be viewed and simply parsed.
53 | - Good, strict type system (as in JSON)
54 | - Good, allows you to build a custom project build process
55 | - Good, powerful templateizer, file import - you can build a project of great complexity.
56 | - Good, has a full-featured Go implementation that can be used as a library
57 | - Bad, little known and few examples on the Internet (ChatGPT knows the language very well:) )
58 | - Bad, when describing a large project - you can get a bulky description file.
--------------------------------------------------------------------------------
/internal/common/infrastructure/executor/executor.go:
--------------------------------------------------------------------------------
1 | package executor
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "io"
7 | "os"
8 | "os/exec"
9 |
10 | "github.com/pkg/errors"
11 |
12 | "github.com/ispringtech/brewkit/internal/common/maybe"
13 | )
14 |
15 | type RunParams struct {
16 | Stdin maybe.Maybe[io.Reader]
17 | Stdout maybe.Maybe[io.Writer]
18 | Stderr maybe.Maybe[io.Writer]
19 | }
20 |
21 | type Executor interface {
22 | Run(ctx context.Context, args Args, params RunParams) error
23 | }
24 |
25 | func New(executable string, opts ...Opt) (Executor, error) {
26 | _, err := exec.LookPath(executable)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | o := options{}
32 | for _, opt := range opts {
33 | opt.apply(&o)
34 | }
35 |
36 | return &executor{
37 | executable: executable,
38 | options: o,
39 | }, nil
40 | }
41 |
42 | type executor struct {
43 | executable string
44 | options options
45 | }
46 |
47 | func (e *executor) Run(ctx context.Context, args Args, params RunParams) (err error) {
48 | cmd := exec.Command(e.executable, args...) // #nosec G204
49 |
50 | cmd.Stdin = maybe.MapNone(params.Stdin, func() io.Reader {
51 | return os.Stdin
52 | })
53 | cmd.Stdout = maybe.MapNone(params.Stdout, func() io.Writer {
54 | return os.Stdout
55 | })
56 | cmd.Stderr = maybe.MapNone(params.Stderr, func() io.Writer {
57 | return os.Stderr
58 | })
59 |
60 | cmd.Env = e.options.env
61 |
62 | err = e.logArgs(args)
63 | if err != nil {
64 | return err
65 | }
66 |
67 | err = cmd.Start()
68 | if err != nil {
69 | return errors.Wrapf(err, "failed to run cmd %s", cmd.String())
70 | }
71 |
72 | commandCtx, cancelFunc := context.WithCancel(context.Background())
73 | defer cancelFunc()
74 |
75 | var runErr error
76 | go func() {
77 | runErr = cmd.Wait()
78 | cancelFunc()
79 | }()
80 |
81 | select {
82 | case <-ctx.Done():
83 | if e.options.logger != nil {
84 | e.options.logger.Info("Stopping process: docker\n")
85 | }
86 |
87 | killErr := cmd.Process.Signal(os.Interrupt)
88 | err = errors.WithStack(ctx.Err())
89 | if killErr != nil {
90 | err = errors.Wrapf(err, killErr.Error())
91 | }
92 | return err
93 | case <-commandCtx.Done():
94 | err = runErr
95 | return err
96 | }
97 | }
98 |
99 | func (e *executor) logArgs(args []string) error {
100 | if e.options.logger == nil {
101 | return nil
102 | }
103 |
104 | bytes, err := json.Marshal(args)
105 | if err != nil {
106 | return errors.Wrap(err, "failed to marshal command args")
107 | }
108 |
109 | e.options.logger.Debug(string(bytes) + "\n")
110 |
111 | return nil
112 | }
113 |
--------------------------------------------------------------------------------
/docs/features.md:
--------------------------------------------------------------------------------
1 | # Features overview
2 |
3 | ## Aggressive-caching
4 |
5 | Aggressive caching lets brewkit achieve faster repeated builds.
6 | When running target BrewKit (or BuildKit under the hood) scans file changes from `copy` and changes
7 | from previous targets to decide if target needs to be executed.
8 |
9 | So if there is no dependency changed and command is the same - there is no need to execute target.
10 |
11 | Exmaple:
12 |
13 | Build-definition for simple go service
14 | ```jsonnet
15 | local app = "service";
16 |
17 | local copy = std.native('copy');
18 |
19 | {
20 | apiVersion: "brewkit/v1",
21 | targets: {
22 | all: ['gobuild'],
23 |
24 | gobuild: {
25 | from: "golang:1.20",
26 | workdir: "/app",
27 | copy: [
28 | copy('cmd', 'cmd'),
29 | copy('pkg', 'pkg'),
30 | ],
31 | command: std.format("go build -o ./bin/%s ./cmd/%s", [app])
32 | }
33 | }
34 | }
35 | ```
36 |
37 | First launch will pull all images, copy artifacts and runs commands
38 |
39 | ```shell
40 | brewkit build
41 | ```
42 |
43 | Output:
44 | ```shell
45 | => [gobuild 4/6] COPY cmd cmd
46 | => [gobuild 5/6] COPY internal internal
47 | => [gobuild 6/6] RUN < CACHED [gobuild 4/6] COPY cmd cmd
54 | => CACHED [gobuild 5/6] COPY internal internal
55 | => CACHED [gobuild 6/6] RUN <
27 | `brewkit` - go-style
28 |
29 | ## Start with BrewKit
30 |
31 | Install BrewKit via go >= 1.20
32 |
33 | ```shell
34 | go install github.com/ispringtech/brewkit/cmd/brewkit@latest
35 | ```
36 |
37 | Create `brewkit.jsonnet`
38 | ```shell
39 | touch brewkit.jsonnet
40 | ```
41 |
42 | Describe simple target
43 | ```jsonnet
44 | local app = "service";
45 |
46 | local copy = std.native('copy');
47 |
48 | {
49 | apiVersion: "brewkit/v1",
50 | targets: {
51 | all: ['gobuild'],
52 |
53 | gobuild: {
54 | from: "golang:1.20",
55 | workdir: "/app",
56 | copy: [
57 | copy('cmd', 'cmd'),
58 | copy('pkg', 'pkg'),
59 | ],
60 | command: std.format("go build -o ./bin/%s ./cmd/%s", [app])
61 | }
62 | }
63 | }
64 | ```
65 |
66 | Run build
67 | ```shell
68 | brewkit build
69 |
70 | => [internal] load build definition from Dockerfile 0.1s
71 | => => transferring dockerfile: 3.45kB 0.0s
72 | => [internal] load .dockerignore 0.1s
73 | => => transferring context: 2B
74 | # ...
75 | ```
76 |
77 | ## Build BrewKit
78 |
79 | When brewkit installed locally
80 | ```shell
81 | brewkit build
82 | ```
83 |
84 | Build from source:
85 | ```shell
86 | go build -o ./bin/brewkit ./cmd/brewkit
87 | ```
88 |
89 | ## Documentation
90 |
91 | * [Documentaion entrypoint](docs/readme.md)
92 |
--------------------------------------------------------------------------------
/internal/backend/infrastructure/docker/outputparser.go:
--------------------------------------------------------------------------------
1 | package docker
2 |
3 | import (
4 | "bufio"
5 | "io"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/pkg/errors"
11 | )
12 |
13 | const (
14 | doneSymbol = "DONE"
15 | )
16 |
17 | var (
18 | outputHeader = regexp.MustCompile(`#\d+ \[.*(?P