diff --git a/.dockerignore b/.dockerignore index 8854504..2ad3ed2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ acme.json acme-backup.json traefik-certs-dumper build-docker.sh +manifest.json diff --git a/.gitignore b/.gitignore index 0cace7a..f890794 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dumpcerts.sh acme.json acme-backup.json traefik-certs-dumper +manifest.json +/*.Dockerfile \ No newline at end of file diff --git a/Makefile b/Makefile index 796401a..6697095 100644 --- a/Makefile +++ b/Makefile @@ -22,5 +22,7 @@ checks: golangci-lint run publish-images: - VERSION=$(TAG_NAME) ./build-docker.sh - VERSION="latest" ./build-docker.sh + go run ./internal/multiarch.go --version="$(TAG_NAME)" + go run ./internal/multiarch.go --version="latest" +# VERSION=$(TAG_NAME) ./build-docker.sh +# VERSION="latest" ./build-docker.sh diff --git a/build-docker.sh b/build-docker.sh index d3bc4ca..f015a03 100755 --- a/build-docker.sh +++ b/build-docker.sh @@ -3,13 +3,15 @@ set -o errexit set -o pipefail +VERSION=v666 + # safe guard -if [ -n "$TRAVIS_TAG" ] && [ -n "$VERSION" ]; then - echo "Deploying..." -else - echo "Skipping deploy" - exit 0 -fi +#if [ -n "$TRAVIS_TAG" ] && [ -n "$VERSION" ]; then +# echo "Deploying..." +#else +# echo "Skipping deploy" +# exit 0 +#fi # base docker image name IMAGE_NAME="ldez/traefik-certs-dumper" @@ -18,7 +20,7 @@ IMAGE_NAME="ldez/traefik-certs-dumper" OS=linux # target platforms in docker manifest notation -declare -a PLATFORMS=( "amd64" "arm.v6" "arm.v7") +declare -a PLATFORMS=( "amd64" "arm.v6" "arm.v7" "arm64") # images from Dockerfile FROM_IMAGE=$(grep "{RUNTIME_HASH}" < Dockerfile | sed "s/FROM //" | sed 's/\$.*//') @@ -43,6 +45,7 @@ function platformHash () { # get manifest if [ ! -f "$MANIFEST_FILE" ]; then docker pull "$FROM_IMAGE" + echo "docker manifest inspect $FROM_IMAGE" DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect "$FROM_IMAGE" > "$MANIFEST_FILE" fi @@ -58,28 +61,28 @@ for platform in "${PLATFORMS[@]}"; do GOARM=${VARIANT:1} # build for target runtime image and architecture +# echo "docker build --build-arg=RUNTIME_HASH=@${RUNTIME_HASH} --build-arg=GOARCH=${ARCHITECTURE} --build-arg=GOARM=${GOARM} -t $IMAGE_NAME:${VERSION}-$platform" . docker build --build-arg="RUNTIME_HASH=@${RUNTIME_HASH}" --build-arg="GOARCH=${ARCHITECTURE}" --build-arg="GOARM=${GOARM}" -t "$IMAGE_NAME:${VERSION}-$platform" . # push images - docker push "$IMAGE_NAME:${VERSION}-$platform" + echo "docker push $IMAGE_NAME:${VERSION}-$platform" +# docker push "$IMAGE_NAME:${VERSION}-$platform" done # create manifest TAG_LIST=$(printf "$IMAGE_NAME:${VERSION}-%s " "${PLATFORMS[@]}") # shellcheck disable=SC2086 -DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create --amend "$IMAGE_NAME:${VERSION}" $TAG_LIST +echo "docker manifest create --amend $IMAGE_NAME:${VERSION} $TAG_LIST" +#DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create --amend "$IMAGE_NAME:${VERSION}" $TAG_LIST for platform in "${PLATFORMS[@]}"; do # split architecture.version IFS='.' read -r ARCHITECTURE VARIANT <<< "$platform" - # docker and go architectures don't match - if [ "arm" == "$ARCHITECTURE" ] && [ -n "$VARIANT" ]; then - VARIANT="$ARCHITECTURE$VARIANT" - fi - - DOCKER_CLI_EXPERIMENTAL=enabled docker manifest annotate "$IMAGE_NAME:${VERSION}" "$IMAGE_NAME:${VERSION}-$platform" --os "$OS" --arch "$ARCHITECTURE" --variant "$VARIANT" + echo "docker manifest annotate $IMAGE_NAME:${VERSION} $IMAGE_NAME:${VERSION}-$platform --os $OS --arch $ARCHITECTURE --variant $VARIANT" +# DOCKER_CLI_EXPERIMENTAL=enabled docker manifest annotate "$IMAGE_NAME:${VERSION}" "$IMAGE_NAME:${VERSION}-$platform" --os "$OS" --arch "$ARCHITECTURE" --variant "$VARIANT" done # push manifest -DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$IMAGE_NAME:${VERSION}" +echo "docker manifest push $IMAGE_NAME:${VERSION}" +#DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$IMAGE_NAME:${VERSION}" diff --git a/go.mod b/go.mod index 6f7e14a..a31d25f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/cpuguy83/go-md2man v1.0.10 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect github.com/fsnotify/fsnotify v1.4.7 github.com/go-acme/lego v2.5.0+incompatible github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect @@ -25,6 +26,8 @@ require ( github.com/jonboulle/clockwork v0.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-testing-interface v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pascaldekloe/goe v0.1.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/prometheus/client_golang v0.9.2 // indirect diff --git a/go.sum b/go.sum index b2b4fa5..136d0a3 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -79,6 +81,10 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= diff --git a/internal/multiarch.go b/internal/multiarch.go new file mode 100644 index 0000000..80877fc --- /dev/null +++ b/internal/multiarch.go @@ -0,0 +1,255 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" + "text/template" + + "github.com/docker/distribution/manifest/manifestlist" +) + +type buildOption struct { + OS string + GoARM string + GoARCH string + Variant string +} + +type actions struct { + Builds [][]string + Push [][]string + ManifestAnnotate [][]string + ManifestCreate []string + ManifestPush []string +} + +func main() { + log.SetFlags(log.Lshortfile) + + imageName := flag.String("image-name", "ldez/traefik-certs-dumper", "") + version := flag.String("version", "", "") + baseImageName := flag.String("base-image-name", "alpine:3.9", "") + dryRun := flag.Bool("dry-run", true, "") + + flag.Parse() + + require("image-name", imageName) + require("version", version) + require("base-image-name", baseImageName) + + // FIXME + // _, travisTag := os.LookupEnv("TRAVIS_TAG") + // if !travisTag { + // log.Println("Skipping deploy") + // os.Exit(0) + // } + + targets := []string{"arm.v6", "arm.v7", "arm.v8", "amd64", "386"} + + actions, err := buildActions(*imageName, *version, *baseImageName, targets) + if err != nil { + log.Fatal(err) + } + + err = execute(actions, *dryRun) + if err != nil { + log.Fatal(err) + } +} + +func require(fieldName string, field *string) { + if field == nil || *field == "" { + log.Fatalf("%s is required", fieldName) + } +} + +func buildActions(imageName, version, baseImageName string, targets []string) (actions, error) { + manifest, err := getManifest(baseImageName) + if err != nil { + return actions{}, err + } + + buildOptions := map[string]buildOption{ + "arm.v5": {OS: "linux", GoARM: "5", GoARCH: "arm", Variant: "v5"}, + "arm.v6": {OS: "linux", GoARM: "6", GoARCH: "arm", Variant: "v6"}, + "arm.v7": {OS: "linux", GoARM: "7", GoARCH: "arm", Variant: "v7"}, + "arm.v8": {OS: "linux", GoARCH: "arm64", Variant: "v8"}, + "amd64": {OS: "linux", GoARCH: "amd64"}, + "386": {OS: "linux", GoARCH: "386"}, + } + + actions := actions{} + + for _, target := range targets { + buildOption := buildOptions[target] + + descriptor, err := findManifestDescriptor(buildOption, manifest.Manifests) + if err != nil { + log.Fatal(err) + } + + dockerfile := fmt.Sprintf("%s-%s-%s.Dockerfile", buildOption.OS, buildOption.GoARCH, buildOption.GoARM) + + actions.Builds = append(actions.Builds, []string{ + "build", + "-t", fmt.Sprintf("%s:%s-%s", imageName, version, target), + "-f", dockerfile, + ".", + }) + + err = createDockerfile(dockerfile, buildOption, descriptor, baseImageName) + if err != nil { + log.Fatal(err) + } + + actions.Push = append(actions.Push, []string{"push", fmt.Sprintf(`%s:%s-%s`, imageName, version, target)}) + + ma := []string{ + "manifest", "annotate", + fmt.Sprintf(`"%s:%s"`, imageName, version), + fmt.Sprintf(`"%s:%s-%s"`, imageName, version, target), + fmt.Sprintf(`--os="%s"`, buildOption.OS), + fmt.Sprintf(`--arch="%s"`, buildOption.GoARCH), + } + if buildOption.Variant != "" { + ma = append(ma, fmt.Sprintf(`--variant="%s"`, buildOption.Variant)) + } + actions.ManifestAnnotate = append(actions.ManifestAnnotate, ma) + } + + actions.ManifestCreate = []string{ + "manifest", "create", "--amend", + fmt.Sprintf("%s:%s", imageName, version), + } + + for _, target := range targets { + actions.ManifestCreate = append(actions.ManifestCreate, fmt.Sprintf(`"%s:%s-%s"`, imageName, version, target)) + } + + actions.ManifestPush = []string{ + "manifest", "push", fmt.Sprintf("%s:%s", imageName, version), + } + + return actions, nil +} + +func createDockerfile(dockerfile string, buildOption buildOption, descriptor manifestlist.ManifestDescriptor, baseImageName string) error { + base := template.New("tmpl.Dockerfile") + parse, err := base.ParseFiles("./internal/tmpl.Dockerfile") + if err != nil { + return err + } + + data := map[string]interface{}{ + "GoOS": buildOption.OS, + "GoARCH": buildOption.GoARCH, + "GoARM": buildOption.GoARM, + "RuntimeImage": fmt.Sprintf("%s@%s", baseImageName, descriptor.Digest), + } + + file, err := os.Create(dockerfile) + if err != nil { + return err + } + + return parse.Execute(file, data) +} + +func getManifest(baseImageName string) (*manifestlist.ManifestList, error) { + manifestPath := "./manifest.json" + + if _, errExist := os.Stat(manifestPath); os.IsNotExist(errExist) { + cmd := exec.Command("docker", "manifest", "inspect", baseImageName) + cmd.Env = append(cmd.Env, "DOCKER_CLI_EXPERIMENTAL=enabled") + + output, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + + err = ioutil.WriteFile(manifestPath, output, 777) + if err != nil { + return nil, err + } + } else if errExist != nil { + return nil, errExist + } + + bytes, err := ioutil.ReadFile(manifestPath) + if err != nil { + return nil, err + } + + manifest := &manifestlist.ManifestList{} + + err = json.Unmarshal(bytes, manifest) + if err != nil { + return nil, err + } + + return manifest, nil +} + +func findManifestDescriptor(criterion buildOption, descriptors []manifestlist.ManifestDescriptor) (manifestlist.ManifestDescriptor, error) { + for _, descriptor := range descriptors { + if descriptor.Platform.OS == criterion.OS && + descriptor.Platform.Architecture == criterion.GoARCH && + descriptor.Platform.Variant == criterion.Variant { + return descriptor, nil + } + } + return manifestlist.ManifestDescriptor{}, fmt.Errorf("not supported: %v", criterion) +} + +func execute(actions actions, dryRun bool) error { + for _, args := range actions.Builds { + if err := execDocker(args, dryRun); err != nil { + return err + } + } + + return nil + + // for _, args := range actions.Push { + // if err := execDocker(args, dryRun); err != nil { + // return err + // } + // } + // + // if err := execDocker(actions.ManifestCreate, dryRun); err != nil { + // return err + // } + // + // for _, args := range actions.ManifestAnnotate { + // if err := execDocker(args, dryRun); err != nil { + // return err + // } + // } + // + // return execDocker(actions.ManifestPush, dryRun) +} + +func execDocker(args []string, dryRun bool) error { + if dryRun { + fmt.Println("docker", strings.Join(args, " ")) + return nil + } + + cmd := exec.Command("docker", args...) + cmd.Env = append(cmd.Env, "DOCKER_CLI_EXPERIMENTAL=enabled") + + output, err := cmd.CombinedOutput() + + log.Println(string(output)) + + if err != nil { + return err + } + return nil +} diff --git a/internal/tmpl.Dockerfile b/internal/tmpl.Dockerfile new file mode 100644 index 0000000..f616f51 --- /dev/null +++ b/internal/tmpl.Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1-alpine as builder + +RUN apk --update upgrade \ + && apk --no-cache --no-progress add git make gcc musl-dev ca-certificates tzdata + +WORKDIR /go/src/github.com/ldez/traefik-certs-dumper + +ENV GO111MODULE on +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN GOARCH={{ .GoARCH }} GOARM={{ .GoARM }} make build + +FROM {{ .RuntimeImage }} + +# Not supported for multi-arch without Buildkit or QEMU +#RUN apk --update upgrade \ +# && apk --no-cache --no-progress add ca-certificates + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=builder /go/src/github.com/ldez/traefik-certs-dumper/traefik-certs-dumper /usr/bin/traefik-certs-dumper + +ENTRYPOINT ["/usr/bin/traefik-certs-dumper"]