diff --git a/.gitignore b/.gitignore index 2b799a6..cda27b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ traefiker +_* + +server/traefik.toml \ No newline at end of file diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..5c3ecf7 --- /dev/null +++ b/config.yml @@ -0,0 +1,13 @@ +--- +docker: + https: # remove this and 443 port before for local dev setup + - john@doe.com + - domain.com + - domain.io + mounts: + - "/var/run/docker.sock:/var/run/docker.sock" + - "./traefik.toml:/traefik.toml" + ports: + - 80:80 + - 443:443 + - 8080:8080 diff --git a/config.yml.example b/config.yml.example deleted file mode 100644 index 1ac2896..0000000 --- a/config.yml.example +++ /dev/null @@ -1,11 +0,0 @@ -traefiker: - name: blinker # Docker container name -docker: - links: ["traefik_mysql_1:mysql", "traefik_redis_1:redis"] # Linked containers, "docker_name:internal_name" format - networks: ["traefik_web"] -labels: - traefik.backend: blinker # Same as traefiker.name above - traefik.docker.network: traefik_web # Same as traefiker.network above - traefik.frontend.rule: Host: linker.gpmd.net - traefik.frontend.entryPoints: https - traefik.port: 80 # exposed port from Docker image diff --git a/traefiker.go b/docker.go similarity index 79% rename from traefiker.go rename to docker.go index a2e6f5e..2aab630 100644 --- a/traefiker.go +++ b/docker.go @@ -11,106 +11,30 @@ import ( "io/ioutil" "log" "os" + "path/filepath" "strconv" "strings" "sync" "time" - "path/filepath" - - "github.com/spf13/viper" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" - - "github.com/shoobyban/filehelper" + "github.com/docker/go-connections/nat" + "github.com/gpmd/filehelper" ) -// AuthInfo stores a users' docker registry/hub info -var AuthInfo sync.Map - -var running = map[string]string{} - -func main() { - - log.Println("Reading configuration...") - viper.SetConfigName("config") - viper.AddConfigPath(".") // optionally look for config in the working directory - err := viper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file - panic(fmt.Errorf("fatal error config file: %v", err)) - } - conf := viper.GetStringMapString("traefiker") - dockerconf := viper.GetStringMapStringSlice("docker") - if conf["network"] != "" && len(dockerconf["networks"]) == 0 { - dockerconf["networks"] = []string{conf["network"]} - } - labelconf := viper.GetStringMapString("labels") - // hotfix for entryPoints - for k, v := range labelconf { - if strings.Contains(k, "entrypoints") { - k2 := strings.Replace(k, "entrypoints", "entryPoints", -1) - delete(labelconf, k) - labelconf[k2] = v - } - } - - log.Println("Connecting to docker...") - - cli, err := client.NewEnvClient() - E(err) - ctx := context.Background() - d := Docker{cli: cli} - - old := map[string]string{} - for _, s := range d.List() { - if s.Image == conf["name"] { - old[s.ID] = s.ID - } - log.Println(s.Image) - running[s.Image] = s.Names[0] - } - - log.Println("Creating docker container...") - - name, err := BuildDockerImage(ctx, conf, d.cli) - E(err) - - d.Run(ctx, name, "", labelconf, dockerconf) - newid := "" - for _, s := range d.List() { - if _, ok := old[s.ID]; !ok { - if s.Image == name { - newid = s.ID - } - } - } - if newid != "" { - for _, id := range old { - log.Println("Stopping", id) - d.StopContainer(ctx, id) - } - } -} - // Docker is base struct for the app type Docker struct { cli *client.Client list []types.Container } -// E is a generic error handler -func E(err error) { - if err != nil { - panic(err) - } -} - // Run starts the created container -func (d *Docker) Run(ctx context.Context, imagename, imageurl string, labels map[string]string, conf map[string][]string) string { +func (d *Docker) Run(ctx context.Context, image, imageurl string, labels map[string]string, conf map[string][]string) string { if imageurl != "" { reader, err := d.cli.ImagePull(ctx, imageurl, types.ImagePullOptions{}) @@ -120,6 +44,12 @@ func (d *Docker) Run(ctx context.Context, imagename, imageurl string, labels map io.Copy(os.Stdout, reader) } + imagename := image + if strings.Contains(imagename, ":") { + parts := strings.Split(imagename, ":") + imagename = parts[0] + } + // nets, err := d.cli.NetworkList(ctx, types.NetworkListOptions{}) // netid := "" // for _, n := range nets { @@ -130,11 +60,18 @@ func (d *Docker) Run(ctx context.Context, imagename, imageurl string, labels map mm := []mount.Mount{} + wd, err := os.Getwd() + if err != nil { + panic(err) + } for _, l := range conf["mounts"] { ll := strings.Split(l, ":") if len(ll) != 2 { log.Panicf("Mounts in config.yml (%v) have line %s where 'from:to' is not correct", conf["mounts"], l) } + if strings.HasPrefix(ll[0], "./") { + ll[0] = wd + strings.TrimPrefix(ll[0], ".") + } mm = append(mm, mount.Mount{ Type: mount.TypeBind, Source: ll[0], @@ -149,16 +86,26 @@ func (d *Docker) Run(ctx context.Context, imagename, imageurl string, labels map var nc *network.NetworkingConfig + type emptyStruct struct{} + + portsMap := make(map[nat.Port]struct{}) + m := make(map[nat.Port][]nat.PortBinding) + if len(conf["ports"]) > 0 { - //hostBinding := nat.PortBinding{ - // HostIP: "0.0.0.0", - // HostPort: "8000", - // } - // containerPort, err := nat.NewPort("tcp", "80") - // if err != nil { - // panic("Unable to get the port") - // } - // hostconfig.PortBindings = nat.PortMap{containerPort: []nat.PortBinding{hostBinding}} + for _, v := range conf["ports"] { + parts := strings.Split(v, ":") + hostBinding := nat.PortBinding{ + HostIP: "0.0.0.0", + HostPort: parts[0], + } + containerPort, err := nat.NewPort("tcp", parts[1]) + if err != nil { + panic("Unable to get the port") + } + portsMap[containerPort] = emptyStruct{} + m[containerPort] = []nat.PortBinding{hostBinding} + } + hostconfig.PortBindings = m } links := []string{} @@ -181,14 +128,18 @@ func (d *Docker) Run(ctx context.Context, imagename, imageurl string, labels map }, } } - + cfg := container.Config{ + Hostname: imagename + ".docker.localhost", + Image: image, + Labels: labels, + ExposedPorts: portsMap, + } + if len(conf["command"]) > 0 { + cfg.Cmd = strslice.StrSlice(conf["command"]) + } cont, err := d.cli.ContainerCreate( context.Background(), - &container.Config{ - Hostname: imagename + ".docker.localhost", - Image: imagename, - Labels: labels, - }, + &cfg, hostconfig, nc, imagename+"_"+strconv.FormatInt(time.Now().UTC().Unix(), 32)) @@ -215,7 +166,7 @@ func (d *Docker) StopContainer(ctx context.Context, containerID string) { E(err) } -// APIClient is meli's client to interact with the docker daemon server +// APIClient is meli's client interface to interact with the docker daemon server type APIClient interface { // we implement this interface so that we can be able to mock it in tests // https://medium.com/@zach_4342/dependency-injection-in-golang-e587c69478a8 @@ -409,10 +360,10 @@ func BuildDockerImage(ctx context.Context, conf map[string]string, cli APIClient ) } if err := scanner.Err(); err != nil { - fmt.Println(" :unable to log output for image", imageName, err) + log.Println(" :unable to log output for image", imageName, err) + return "", fmt.Errorf("unable to build due logging error %v", err) } imageBuildResponse.Body.Close() - log.Println("Build successful") return imageName, nil } diff --git a/go.mod b/go.mod index d4e1d1d..5760843 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,16 @@ module github.com/gpmd/traefiker go 1.13 require ( + github.com/coreos/etcd v3.3.10+incompatible github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v1.13.1 github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect + github.com/gpmd/filehelper v0.3.0 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/shoobyban/filehelper v0.0.0-20190509122806-291f797b4fe9 + github.com/shoobyban/slog v0.0.0-20190209173919-7f513f7a44c1 github.com/spf13/viper v1.4.0 golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect ) diff --git a/go.sum b/go.sum index e1e402b..397a3bc 100644 --- a/go.sum +++ b/go.sum @@ -9,7 +9,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -40,13 +42,17 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gpmd/filehelper v0.3.0 h1:uQ5f4WKzw9AKrcOyN0ovUFhSN4Yat/wirJWxIQvyngc= +github.com/gpmd/filehelper v0.3.0/go.mod h1:0G5XdBTA7E7LP/d0+sJOdWbJWNdQoQgP7ndK6/kK9D4= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= @@ -91,6 +97,8 @@ github.com/shoobyban/mxj v1.8.5/go.mod h1:wxi+v21Kjjno03XA3hlnEPxGfZS0Gra+fuXWXw github.com/shoobyban/slog v0.0.0-20190209173919-7f513f7a44c1 h1:mPLIPkJhNNBl477M8UhiyzmZRgNyBSx9lOeevhm3VYw= github.com/shoobyban/slog v0.0.0-20190209173919-7f513f7a44c1/go.mod h1:262Gnlo2g3UKb2sfzPx6aWuybU0xilZraiw7SPUXRIU= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= @@ -108,6 +116,7 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= @@ -143,6 +152,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4803cc6 --- /dev/null +++ b/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/spf13/viper" + + "github.com/docker/docker/client" +) + +// AuthInfo stores a users' docker registry/hub info +var AuthInfo sync.Map + +var running = map[string]string{} + +// E is a generic error handler +func E(err error) { + if err != nil { + panic(err) + } +} + +func main() { + + log.Println("Reading configuration...") + viper.SetConfigName("config") + viper.AddConfigPath(".") // optionally look for config in the working directory + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + panic(fmt.Errorf("fatal error config file: %v", err)) + } + conf := viper.GetStringMapString("traefiker") + dockerconf := viper.GetStringMapStringSlice("docker") + if conf["network"] != "" && len(dockerconf["networks"]) == 0 { + dockerconf["networks"] = []string{conf["network"]} + } + labelconf := viper.GetStringMapString("labels") + // hotfix for entryPoints + for k, v := range labelconf { + if strings.Contains(k, "entrypoints") { + k2 := strings.Replace(k, "entrypoints", "entryPoints", -1) + delete(labelconf, k) + labelconf[k2] = v + } + } + + log.Println("Connecting to docker...") + + cli, err := client.NewEnvClient() + E(err) + ctx := context.Background() + d := Docker{cli: cli} + + if len(os.Args) > 1 { + switch os.Args[1] { + case "server": + server() + case "traefik": + traefik(ctx, d, dockerconf) + } + return + } + + old := map[string]string{} + for _, s := range d.List() { + if s.Image == conf["name"] || strings.HasPrefix(s.Names[0], "/"+conf["name"]+"_") { + old[s.ID] = s.ID + } + log.Println(s.Image) + running[s.Image] = s.Names[0] + } + + log.Println("Creating docker container...") + + name, err := BuildDockerImage(ctx, conf, d.cli) + E(err) + + d.Run(ctx, name, "", labelconf, dockerconf) + + time.Sleep(2 * time.Second) + + newid := "" + for _, s := range d.List() { + if _, ok := old[s.ID]; !ok { + if s.Image == name { + newid = s.ID + } + } + } + + if newid == "" { + log.Println("Unsuccessful build, container is not running") + os.Exit(-1) + } + + for _, id := range old { + log.Println("Stopping", id) + d.StopContainer(ctx, id) + } + +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..a0c1ffa --- /dev/null +++ b/server.go @@ -0,0 +1,5 @@ +package main + +func server() { + +} diff --git a/traefik.go b/traefik.go new file mode 100644 index 0000000..d464a3d --- /dev/null +++ b/traefik.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "io/ioutil" + "os" + "strings" + + "github.com/gpmd/filehelper" + "github.com/shoobyban/slog" +) + +func traefik(ctx context.Context, d Docker, dockerconf map[string][]string) { + t, err := filehelper.ProcessTemplateFile("traefik.toml.template", dockerconf) + if err != nil { + panic(err) + } + os.MkdirAll("server", 0755) + err = ioutil.WriteFile("server/traefik.toml", t, 0755) + if err != nil { + panic(err) + } + var id string + for _, s := range d.List() { + if s.Image == "traefik:latest" || strings.HasPrefix(s.Names[0], "/traefik_") { + slog.Infof("names %v", s.Names) + id = s.ID + break + } + } + if id != "" { + d.StopContainer(ctx, id) + } + os.Chdir("server") + d.Run(ctx, "traefik:latest", "docker.io/library/traefik:latest", map[string]string{}, dockerconf) +} diff --git a/traefik.toml.template b/traefik.toml.template new file mode 100644 index 0000000..dc4e592 --- /dev/null +++ b/traefik.toml.template @@ -0,0 +1,45 @@ +defaultEntryPoints = ["http"{{if .https}}, "https"{{end}}] +{{if .log}} +[log] + level = "debug" + +[accessLog] + filePath = "access.log" +{{end}} +[entryPoints] + [entryPoints.http] + address = ":80" + +{{if .https}} + [entryPoints.https] + address = ":443" +{{end}} + +[api] + dashboard = true + insecure = true +{{if .log}} debug = true{{end}} + +[providers] + [providers.docker] + exposedByDefault = false + network = "web" + endpoint = "unix:///var/run/docker.sock" + watch = true +{{if .https}} +[tls.stores] + [tls.stores.default] + +[certificatesResolvers.le.acme] + email = "{{index .https 0}}" + storage = "acme/acme.json" + [certificatesResolvers.le.acme.httpChallenge] + entryPoint = "http" + +[[acme.domains]] + main = "{{index .https 1}}" +{{if gt (len .https) 2}} sans = [ {{range $k, $v := .https}}{{if gt $k 1}}{{if gt $k 2}},{{end}}"{{$v}}"{{end}}{{end}} ]{{end}} + +[acme] + caServer = "https://acme-v02.api.letsencrypt.org/directory" +{{end}}