Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docker: Add registry support #84

Merged
merged 1 commit into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ $ bump current
Dockerfile:1: alpine 3.9.2
# See possible updates
$ bump check
alpine 3.14.2
alpine 3.16.2
# See what will be changed
$ bump diff
--- Dockerfile
+++ Dockerfile
@@ -1,2 +1,2 @@
-FROM alpine:3.9.2 AS builder
+FROM alpine:3.14.2 AS builder
+FROM alpine:3.16.2 AS builder

# Write changes
$ bump update
Expand Down Expand Up @@ -275,13 +275,13 @@ can be helpful when testing pipelines.
```sh (exec)
# Latest 4.0 ffmpeg version
$ bump pipeline 'https://github.com/FFmpeg/FFmpeg.git|^4'
4.4.1
4.4.3
# Commit hash of the latest 4.0 ffmpeg version
$ bump pipeline 'https://github.com/FFmpeg/FFmpeg.git|^4|@commit'
2aa4f5cc8be5e3168191cd13a61178a167687eac
3d69f9682f06bbf72e0cdcdc9e66c9307ed6b24f
# Latest 1.0 golang docker build image
$ bump pipeline 'docker:golang|^1'
1.17.3
1.19.2
# Latest mp3lame version
$ bump pipeline 'svn:http://svn.code.sf.net/p/lame/svn|/^RELEASE__(.*)$/|/_/./|*'
3.100
Expand Down Expand Up @@ -316,9 +316,7 @@ Use gitrefs filter to get all refs unfiltered.

```sh
$ bump pipeline 'https://github.com/git/git.git|*'
2.33.1
$ bump pipeline 'git://github.com/git/git.git|*'
2.33.1
2.38.1
```

### gitrefs<span id="filter-gitrefs">
Expand All @@ -339,11 +337,16 @@ HEAD

`docker:<image>`

Produce versions from a image on ducker hub.
Produce versions from a image on ducker hub or other registry.
Currently only supports anonymous access.

```sh
$ bump pipeline 'docker:alpine|^3'
3.14.2
3.16.2
$ bump pipeline 'docker:mwader/static-ffmpeg|^4'
4.4.1
$ bump pipeline 'docker:ghcr.io/nginx-proxy/nginx-proxy|^0.9'
0.9.3
```

### svn<span id="filter-svn">
Expand All @@ -355,7 +358,7 @@ be the tag or branch name, version the revision.

```sh
$ bump pipeline 'svn:https://svn.apache.org/repos/asf/subversion|*'
1.14.1
1.14.2
```

### fetch<span id="filter-fetch">
Expand Down
160 changes: 160 additions & 0 deletions internal/dockerv2/dockerv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package dockerv2

import (
"encoding/csv"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)

type Registry struct {
Host string
Image string
Token string
}

var defaultRegistry = Registry{
Host: "index.docker.io",
}

const listTagsURLTemplate = `https://%s/v2/%s/tags/list`

func NewFromImage(image string) (*Registry, error) {
parts := strings.Split(image, "/")
r := defaultRegistry
switch {
case len(parts) == 0:
return &r, fmt.Errorf("invalid image")
case len(parts) == 1:
// image
r.Image = "library/" + image
return &r, nil
default:
}

if strings.Contains(parts[0], ".") {
// host.tldr/image
r.Host = parts[0]
r.Image = strings.Join(parts[1:], "/")
return &r, nil
}

// repo/image
r.Image = image
return &r, nil
}

// The WWW-Authenticate Response Header Field
// https://www.rfc-editor.org/rfc/rfc6750#section-3
type WWWAuth struct {
Scheme string
Params map[string]string
}

// WWW-Authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:org/image:pull""
func ParseWWWAuth(s string) (WWWAuth, error) {
var w WWWAuth
parts := strings.SplitN(s, " ", 2)
if len(parts) != 2 {
return WWWAuth{}, fmt.Errorf("invalid params")
}
w.Scheme = parts[0]

r := csv.NewReader(strings.NewReader(strings.TrimSpace(parts[1])))
// allows mix quotes and explicit ","
r.LazyQuotes = true
r.Comma = rune(',')
pairs, pairsErr := r.Read()
if pairsErr != nil {
return WWWAuth{}, pairsErr
}

w.Params = map[string]string{}
for _, p := range pairs {
r := csv.NewReader(strings.NewReader(p))
r.Comma = rune('=')
kv, kvErr := r.Read()
if kvErr != nil {
return WWWAuth{}, kvErr
}
if len(kv) != 2 {
return WWWAuth{}, fmt.Errorf("invalid pair")
}
w.Params[kv[0]] = kv[1]
}

return w, nil
}

func get(rawURL string, doAuth bool, token string, out interface{}) error {
req, err := http.NewRequest(http.MethodGet, rawURL, nil)
if err != nil {
return err
}

if token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}

r, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer r.Body.Close()

if r.StatusCode/100 == 4 {
if doAuth && r.StatusCode == 401 {
wwwAuth := r.Header.Get("WWW-Authenticate")
if wwwAuth == "" {
return fmt.Errorf("no WWW-Authenticate found")
}

w, wwwAuthErr := ParseWWWAuth(wwwAuth)
if wwwAuthErr != nil {
return wwwAuthErr
}

authURLValues := url.Values{}
authURLValues.Set("service", w.Params["service"])
authURLValues.Set("scope", w.Params["scope"])
authURL, authURLErr := url.Parse(w.Params["realm"])
if authURLErr != nil {
return authURLErr
}
authURL.RawQuery = authURLValues.Encode()

var authResp struct {
Token string `json:"token"`
}
authTokenErr := get(authURL.String(), false, "", &authResp)
if authTokenErr != nil {
return authTokenErr
}

return get(rawURL, false, authResp.Token, out)
}
return fmt.Errorf(r.Status)
}

if r.StatusCode/100 != 2 {
return fmt.Errorf("error response: %s", r.Status)
}

if err := json.NewDecoder(r.Body).Decode(&out); err != nil {
return fmt.Errorf("failed parse response: %w", err)
}

return nil
}

func (r *Registry) Tags() ([]string, error) {
var resp struct {
Tags []string `json:"tags"`
}
if err := get(fmt.Sprintf(listTagsURLTemplate, r.Host, r.Image), true, "", &resp); err != nil {
return nil, err
}
return resp.Tags, nil
}
20 changes: 20 additions & 0 deletions internal/dockerv2/dockerv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dockerv2_test

import (
"testing"

"github.com/wader/bump/internal/dockerv2"
)

func TestParseWWWAuth(t *testing.T) {
w, err := dockerv2.ParseWWWAuth(`Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:org/image:pull"`)
if err != nil {
t.Fatal(err)
}
if w.Scheme != "Bearer" {
t.Fatalf("schema %s", w.Scheme)
}
if v := w.Params["service"]; v != "ghcr.io" {
t.Fatalf("service %s", v)
}
}
91 changes: 15 additions & 76 deletions internal/filter/docker/docker.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package docker

import (
"encoding/json"
"fmt"
"net/http"
"strings"

"github.com/wader/bump/internal/dockerv2"
"github.com/wader/bump/internal/filter"
)

Expand All @@ -16,30 +14,14 @@ const Name = "docker"
var Help = `
docker:<image>

Produce versions from a image on ducker hub.
Produce versions from a image on ducker hub or other registry.
Currently only supports anonymous access.

docker:alpine|^3
docker:mwader/static-ffmpeg|^4
docker:ghcr.io/nginx-proxy/nginx-proxy|^0.9
`[1:]

// TODO: support other registries
// TODO: auth?
const authURLTemplate = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull`
const listTagsURLTemplate = `https://index.docker.io/v2/%s/tags/list`

// image -> library/image
// repo/image -> repo/image
func argToRepoImage(a string) (string, error) {
parts := strings.Split(a, "/")
switch {
case len(parts) == 0:
return "", fmt.Errorf("invalid name")
case len(parts) == 1:
return "library/" + parts[0], nil
default:
return a, nil
}
}

// New docker filter
func New(prefix string, arg string) (filter filter.Filter, err error) {
if prefix != Name {
Expand All @@ -49,77 +31,34 @@ func New(prefix string, arg string) (filter filter.Filter, err error) {
return nil, fmt.Errorf("needs a image name")
}

repoImage, err := argToRepoImage(arg)
registry, err := dockerv2.NewFromImage(arg)
if err != nil {
return nil, fmt.Errorf("%w: %s", err, arg)
}

return dockerFilter{
imageName: arg,
repoImage: repoImage,
image: arg,
registry: registry,
}, nil
}

type dockerFilter struct {
imageName string
repoImage string
image string
registry *dockerv2.Registry
}

func (f dockerFilter) String() string {
return Name + ":" + f.imageName
}

func (f dockerFilter) getToken() (string, error) {
r, err := http.Get(fmt.Sprintf(authURLTemplate, f.repoImage))
if err != nil {
return "", fmt.Errorf("token request failed: %w", err)
}
defer r.Body.Close()

var resp struct {
Token string `json:"token"`
}

if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("failed to parse token response: %w", err)
}

return resp.Token, nil
return Name + ":" + f.image
}

func (f dockerFilter) Filter(versions filter.Versions, versionKey string) (filter.Versions, string, error) {
token, err := f.getToken()
if err != nil {
return nil, "", err
}

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(listTagsURLTemplate, f.repoImage), nil)
if err != nil {
return nil, "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))

r, err := http.DefaultClient.Do(req)
if err != nil {
return nil, versionKey, err
}
defer r.Body.Close()

if r.StatusCode/100 != 2 {
return nil, "", fmt.Errorf("error response: %s", r.Status)
}

var resp struct {
Tags []string `json:"tags"`
}

err = json.NewDecoder(r.Body).Decode(&resp)
if err != nil {
return nil, "", err
tags, tagsErr := f.registry.Tags()
if tagsErr != nil {
return filter.Versions{}, "", tagsErr
}

tagNames := append(filter.Versions{}, versions...)
for _, t := range resp.Tags {
for _, t := range tags {
tagNames = append(tagNames, filter.NewVersionWithName(t, nil))
}

Expand Down
1 change: 0 additions & 1 deletion internal/filter/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ the version found in the tag, commit the commit hash or tag object.
Use gitrefs filter to get all refs unfiltered.

https://github.com/git/git.git|*
git://github.com/git/git.git|*
`[1:]

// default ref filter
Expand Down
Loading