-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Chris Goller <[email protected]>
- Loading branch information
Showing
9 changed files
with
867 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package push | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/containerd/containerd/reference" | ||
"github.com/containerd/containerd/remotes/docker" | ||
"github.com/containerd/containerd/remotes/docker/auth" | ||
depotapi "github.com/depot/cli/pkg/api" | ||
"github.com/docker/cli/cli/command" | ||
configtypes "github.com/docker/cli/cli/config/types" | ||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
func GetAuthToken(ctx context.Context, dockerCli command.Cli, parsedTag *ParsedTag, manifest ocispecs.Descriptor) (*Token, error) { | ||
authConfig, err := GetAuthConfig(dockerCli, parsedTag.Host) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
push := true | ||
scope, err := docker.RepositoryScope(parsedTag.Refspec, push) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
challenge, err := AuthKind(ctx, parsedTag.Refspec, manifest) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return FetchToken(ctx, authConfig, challenge, []string{scope}) | ||
} | ||
|
||
func GetAuthConfig(dockerCli command.Cli, host string) (*configtypes.AuthConfig, error) { | ||
if host == "registry-1.docker.io" { | ||
host = "https://index.docker.io/v1/" | ||
} | ||
|
||
config, err := dockerCli.ConfigFile().GetAuthConfig(host) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &config, nil | ||
} | ||
|
||
// AuthKind tries to do a HEAD request to the manifest to try to get the WWW-Authenticate header. | ||
// If HEAD is not supported, it will try to get a GET. Apparently, this is for older registries. | ||
func AuthKind(ctx context.Context, refspec reference.Spec, manifest ocispecs.Descriptor) (*auth.Challenge, error) { | ||
// Reversing the refspec's path.Join behavior. | ||
i := strings.Index(refspec.Locator, "/") | ||
host, repository := refspec.Locator[:i], refspec.Locator[i+1:] | ||
if host == "docker.io" { | ||
host = "registry-1.docker.io" | ||
} | ||
|
||
url := fmt.Sprintf("https://%s/v2/%s/manifests/%s", host, repository, refspec.Object) | ||
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
req.Header.Add("Accept", manifest.MediaType) | ||
req.Header.Add("Accept", `*/*`) | ||
req.Header.Set("User-Agent", depotapi.Agent()) | ||
|
||
// Helper function allowing the HTTP method to change because some registries | ||
// use GET rather than HEAD (according to an old comment). | ||
return checkAuthKind(ctx, req) | ||
} | ||
|
||
func checkAuthKind(ctx context.Context, req *http.Request) (*auth.Challenge, error) { | ||
res, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
_ = res.Body.Close() | ||
switch res.StatusCode { | ||
case http.StatusOK: | ||
return nil, nil | ||
case http.StatusUnauthorized: | ||
challenges := auth.ParseAuthHeader(res.Header) | ||
if len(challenges) == 0 { | ||
return nil, nil | ||
} | ||
return &challenges[0], nil | ||
case http.StatusMethodNotAllowed: | ||
// We have a callback here to allow us to retry the request with a `GET`if the registry doesn't support `HEAD`. | ||
req.Method = http.MethodGet | ||
return checkAuthKind(ctx, req) | ||
case http.StatusRequestTimeout, http.StatusTooManyRequests: | ||
// TODO: backoff and retry. | ||
} | ||
|
||
return nil, fmt.Errorf("unexpected status code: %s", res.Status) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package push | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/opencontainers/go-digest" | ||
) | ||
|
||
type BlobRequest struct { | ||
ParsedTag *ParsedTag | ||
RegistryToken *Token | ||
BuildID string | ||
Digest digest.Digest | ||
} | ||
|
||
func PushBlob(ctx context.Context, depotToken string, req *BlobRequest) error { | ||
pushRequest := struct { | ||
RegistryHost string `json:"registryHost"` | ||
RepositoryNamespace string `json:"repositoryNamespace"` | ||
RegistryToken string `json:"registryToken"` | ||
TokenScheme string `json:"tokenScheme"` | ||
}{ | ||
RegistryHost: req.ParsedTag.Host, | ||
RepositoryNamespace: req.ParsedTag.Path, | ||
RegistryToken: req.RegistryToken.Token, | ||
TokenScheme: req.RegistryToken.Scheme, | ||
} | ||
buf, _ := json.MarshalIndent(pushRequest, "", " ") | ||
|
||
url := fmt.Sprintf("https://blob.depot.dev/blobs/%s/%s", req.BuildID, req.Digest.String()) | ||
pushReq, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(buf))) | ||
if err != nil { | ||
return err | ||
} | ||
pushReq.Header.Add("Authorization", "Bearer "+depotToken) | ||
pushReq.Header.Set("Content-Type", "application/json") | ||
resp, err := http.DefaultClient.Do(pushReq) | ||
if err != nil { | ||
return err | ||
} | ||
_ = resp.Body.Close() | ||
if resp.StatusCode/100 != 2 { | ||
body, _ := io.ReadAll(resp.Body) | ||
err := fmt.Errorf("unexpected status code: %d %s", resp.StatusCode, string(body)) | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package push | ||
|
||
import ( | ||
"github.com/containerd/containerd/reference" | ||
"github.com/containerd/containerd/remotes/docker" | ||
ref "github.com/distribution/reference" | ||
) | ||
|
||
type ParsedTag struct { | ||
Host string | ||
Path string | ||
Refspec reference.Spec | ||
Tag string | ||
} | ||
|
||
func ParseTag(tag string) (*ParsedTag, error) { | ||
named, err := ref.ParseNormalizedNamed(tag) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
domain := ref.Domain(named) | ||
path := ref.Path(named) | ||
named = ref.TagNameOnly(named) | ||
var imageTag string | ||
if r, ok := named.(ref.Tagged); ok { | ||
imageTag = r.Tag() | ||
} | ||
|
||
host, _ := docker.DefaultHost(domain) | ||
|
||
refspec, err := reference.Parse(named.String()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &ParsedTag{Host: host, Path: path, Refspec: refspec, Tag: imageTag}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
package push | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/bufbuild/connect-go" | ||
"github.com/containerd/containerd/errdefs" | ||
"github.com/containerd/containerd/images" | ||
"github.com/containerd/containerd/reference" | ||
"github.com/containerd/containerd/remotes" | ||
"github.com/containerd/containerd/remotes/docker" | ||
depotapi "github.com/depot/cli/pkg/api" | ||
"github.com/depot/cli/pkg/progress" | ||
cliv1 "github.com/depot/cli/pkg/proto/depot/cli/v1" | ||
"github.com/opencontainers/go-digest" | ||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
func PushManifest(ctx context.Context, registryToken string, refspec reference.Spec, tag string, manifest ocispecs.Descriptor, manifestBytes []byte) error { | ||
// Reversing the refspec's path.Join behavior. | ||
i := strings.Index(refspec.Locator, "/") | ||
host, repository := refspec.Locator[:i], refspec.Locator[i+1:] | ||
if host == "docker.io" { | ||
host = "registry-1.docker.io" | ||
} | ||
|
||
url := fmt.Sprintf("https://%s/v2/%s/manifests/%s", host, repository, tag) | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(manifestBytes)) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Set("User-Agent", depotapi.Agent()) | ||
req.Header.Set("Content-Type", manifest.MediaType) | ||
// TODO: | ||
req.Header.Set("Authorization", "Bearer "+registryToken) | ||
|
||
res, err := http.DefaultClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { _ = res.Body.Close() }() | ||
if res.StatusCode/100 == 2 { | ||
return nil | ||
} | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return err | ||
} | ||
return fmt.Errorf("unexpected status code: %s %s", res.Status, string(body)) | ||
} | ||
|
||
type ImageDescriptors struct { | ||
Indices []ocispecs.Descriptor `json:"indices,omitempty"` | ||
Manifests []ocispecs.Descriptor `json:"manifests,omitempty"` | ||
Configs []ocispecs.Descriptor `json:"configs,omitempty"` | ||
Layers []ocispecs.Descriptor `json:"layers,omitempty"` | ||
|
||
IndexBytes map[digest.Digest][]byte `json:"indexBytes,omitempty"` | ||
ManifestBytes map[digest.Digest][]byte `json:"manifestBytes,omitempty"` | ||
} | ||
|
||
// GetImageDescriptors returns back all the descriptors for an image. | ||
func GetImageDescriptors(ctx context.Context, token, buildID string, logger progress.StartLogDetailFunc) (*ImageDescriptors, error) { | ||
// Download location and credentials of ephemeral image save. | ||
client := depotapi.NewBuildClient() | ||
req := &cliv1.GetPullInfoRequest{BuildId: buildID} | ||
res, err := client.GetPullInfo(ctx, depotapi.WithAuthentication(connect.NewRequest(req), token)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
username, password, ref := res.Msg.Username, res.Msg.Password, res.Msg.Reference | ||
|
||
authorizer := &Authorizer{Username: username, Password: password} | ||
hosts := docker.ConfigureDefaultRegistries(docker.WithAuthorizer(authorizer)) | ||
|
||
headers := http.Header{} | ||
headers.Set("User-Agent", depotapi.Agent()) | ||
resolver := docker.NewResolver(docker.ResolverOptions{ | ||
Hosts: hosts, | ||
Headers: headers, | ||
}) | ||
|
||
fin := logger(fmt.Sprintf("Resolving %s", ref)) | ||
name, desc, err := resolver.Resolve(ctx, ref) | ||
fin() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
fin = logger(fmt.Sprintf("Fetching %s", name)) | ||
fetcher, err := resolver.Fetcher(ctx, name) | ||
fin() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
stack := []ocispecs.Descriptor{desc} | ||
descs := ImageDescriptors{ | ||
IndexBytes: map[digest.Digest][]byte{}, | ||
ManifestBytes: map[digest.Digest][]byte{}, | ||
} | ||
|
||
for len(stack) > 0 { | ||
desc, stack = stack[len(stack)-1], stack[:len(stack)-1] | ||
|
||
// Only download unique descriptors. | ||
if _, ok := descs.IndexBytes[desc.Digest]; ok { | ||
continue | ||
} | ||
if _, ok := descs.ManifestBytes[desc.Digest]; ok { | ||
continue | ||
} | ||
|
||
fin = logger(fmt.Sprintf("Fetching manifest %s", desc.Digest.String())) | ||
buf, err := fetch(ctx, fetcher, desc) | ||
fin() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if images.IsIndexType(desc.MediaType) { | ||
descs.Indices = append(descs.Indices, desc) | ||
var index ocispecs.Index | ||
if err := json.Unmarshal(buf, &index); err != nil { | ||
return nil, err | ||
} | ||
|
||
descs.IndexBytes[desc.Digest] = buf | ||
|
||
for _, m := range index.Manifests { | ||
if m.Digest != "" { | ||
stack = append(stack, m) | ||
} | ||
} | ||
} else if images.IsManifestType(desc.MediaType) { | ||
descs.Manifests = append(descs.Manifests, desc) | ||
var manifest ocispecs.Manifest | ||
if err := json.Unmarshal(buf, &manifest); err != nil { | ||
return nil, err | ||
} | ||
|
||
descs.ManifestBytes[desc.Digest] = buf | ||
|
||
descs.Configs = append(descs.Configs, manifest.Config) | ||
descs.Layers = append(descs.Layers, manifest.Layers...) | ||
} | ||
} | ||
|
||
return &descs, nil | ||
} | ||
|
||
func fetch(ctx context.Context, fetcher remotes.Fetcher, desc ocispecs.Descriptor) ([]byte, error) { | ||
r, err := fetcher.Fetch(ctx, desc) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer func() { _ = r.Close() }() | ||
return io.ReadAll(r) | ||
} | ||
|
||
type Authorizer struct { | ||
Username string | ||
Password string | ||
} | ||
|
||
func (a *Authorizer) Authorize(ctx context.Context, req *http.Request) error { | ||
req.SetBasicAuth(a.Username, a.Password) | ||
return nil | ||
} | ||
func (a *Authorizer) AddResponses(ctx context.Context, responses []*http.Response) error { | ||
return errdefs.ErrNotImplemented | ||
} |
Oops, something went wrong.