diff --git a/pkg/load/load.go b/pkg/load/load.go index fd4d63ea..cb085aba 100644 --- a/pkg/load/load.go +++ b/pkg/load/load.go @@ -8,7 +8,6 @@ import ( "fmt" "runtime" "strings" - "time" depotbuild "github.com/depot/cli/pkg/buildx/build" depotprogress "github.com/depot/cli/pkg/progress" @@ -33,40 +32,44 @@ func DepotFastLoad(ctx context.Context, dockerapi docker.APIClient, resp []depot nodeRes := chooseNodeResponse(buildRes.NodeResponses) pullOpt := pullOpts[buildRes.Name] - architecture := nodeRes.Node.DriverOpts["platform"] - manifest, config, err := decodeNodeResponse(architecture, nodeRes) - if err != nil { - return err + digest := nodeRes.SolveResponse.ExporterResponse[exptypes.ExporterImageDigestKey] + if v, ok := nodeRes.SolveResponse.ExporterResponse[exptypes.ExporterImageConfigDigestKey]; ok { + digest = v + } + if digest == "" { + return errors.New("missing image digest") } - proxyOpts := &ProxyConfig{ - RawManifest: manifest, - RawConfig: config, - Addr: nodeRes.Node.DriverOpts["addr"], - CACert: []byte(nodeRes.Node.DriverOpts["caCert"]), - Key: []byte(nodeRes.Node.DriverOpts["key"]), - Cert: []byte(nodeRes.Node.DriverOpts["cert"]), + + info := struct { + Address string `json:"address"` + Cert string `json:"cert"` + Key string `json:"key"` + CaCert string `json:"caCert"` + }{ + Address: nodeRes.Node.DriverOpts["addr"], + Cert: base64.StdEncoding.EncodeToString([]byte(nodeRes.Node.DriverOpts["cert"])), + Key: base64.StdEncoding.EncodeToString([]byte(nodeRes.Node.DriverOpts["key"])), + CaCert: base64.StdEncoding.EncodeToString([]byte(nodeRes.Node.DriverOpts["caCert"])), } - // Start the depot registry proxy. - var registry *RegistryProxy - err = progress.Wrap("preparing to load", pw.Write, func(logger progress.SubLogger) error { - registry, err = NewRegistryProxy(ctx, proxyOpts, dockerapi) - if err != nil { - err = logger.Wrap(fmt.Sprintf("[registry] unable to start: %s", err), func() error { return err }) - } - return err - }) + username := "x-info" + passwordBytes, err := json.Marshal(info) if err != nil { - return err + return fmt.Errorf("failed to marshal info: %w", err) } - defer func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - registry.Close(ctx) - cancel() - }() + password := string(passwordBytes) + serverAddress := "depot-pull.fly.dev" // TODO: move this to the API + + pullOpt.Username = &username + pullOpt.Password = &password + pullOpt.ServerAddress = &serverAddress + + randomImageName := RandImageName() + tag := "manifest" + imageToPull := fmt.Sprintf("%s/%s:%s@%s", serverAddress, randomImageName, tag, digest) // Pull the image and relabel it with the user specified tags. - err = PullImages(ctx, dockerapi, registry.ImageToPull, pullOpt, pw) + err = PullImages(ctx, dockerapi, imageToPull, pullOpt, pw) if err != nil { return fmt.Errorf("failed to pull image: %w", err) } @@ -94,44 +97,6 @@ func chooseNodeResponse(nodeResponses []depotbuild.DepotNodeResponse) depotbuild // ImageExported is the solve response key added for `depot.export.image.version=2`. const ImagesExported = "depot/images.exported" -func decodeNodeResponse(architecture string, nodeRes depotbuild.DepotNodeResponse) (rawManifest, rawConfig []byte, err error) { - if _, err := EncodedExportedImages(nodeRes.SolveResponse.ExporterResponse); err == nil { - return decodeNodeResponseV2(architecture, nodeRes) - } - - // Needed until all depot builds and CLI versions are updated. - return decodeNodeResponseV1(architecture, nodeRes) -} - -func decodeNodeResponseV2(architecture string, nodeRes depotbuild.DepotNodeResponse) (rawManifest, rawConfig []byte, err error) { - encodedExportedImages, err := EncodedExportedImages(nodeRes.SolveResponse.ExporterResponse) - if err != nil { - return nil, nil, err - } - - exportedImages, _, imageConfigs, err := DecodeExportImages(encodedExportedImages) - if err != nil { - return nil, nil, err - } - - idx, err := chooseBestImageManifestV2(architecture, imageConfigs) - if err != nil { - return nil, nil, err - } - - return exportedImages[idx].Manifest, exportedImages[idx].Config, nil -} - -// EncodedExportedImages returns the encoded exported images from the solve response. -// This uses the `depot.export.image.version=2` format. -func EncodedExportedImages(exporterResponse map[string]string) (string, error) { - encodedExportedImages, ok := exporterResponse[ImagesExported] - if !ok { - return "", errors.New("missing image export response") - } - return encodedExportedImages, nil -} - // RawExportedImage is the JSON-encoded image manifest and config used loading the image. type RawExportedImage struct { // JSON-encoded ocispecs.Manifest. @@ -177,162 +142,3 @@ func DecodeExportImages(encodedExportedImages string) ([]RawExportedImage, []oci return exportedImages, manifests, imageConfigs, nil } - -// We encode the image manifest and image config within the buildkitd Solve response -// because the content may be GCed by the time this load occurs. -func decodeNodeResponseV1(architecture string, nodeRes depotbuild.DepotNodeResponse) (rawManifest, rawConfig []byte, err error) { - encodedDesc, ok := nodeRes.SolveResponse.ExporterResponse[exptypes.ExporterImageDescriptorKey] - if !ok { - return nil, nil, errors.New("missing image descriptor") - } - - jsonImageDesc, err := base64.StdEncoding.DecodeString(encodedDesc) - if err != nil { - return nil, nil, fmt.Errorf("invalid image descriptor: %w", err) - } - - var imageDesc ocispecs.Descriptor - if err := json.Unmarshal(jsonImageDesc, &imageDesc); err != nil { - return nil, nil, fmt.Errorf("invalid image descriptor json: %w", err) - } - - var imageManifest ocispecs.Descriptor = imageDesc - { - // These checks handle situations where the image does and does not have attestations. - // If there are no attestations, then the imageDesc contains the manifest and config. - // Otherwise the imageDesc's `depot.containerimage.index` will contain the manifest and config. - - encodedIndex, ok := imageDesc.Annotations["depot.containerimage.index"] - if ok { - var index ocispecs.Index - if err := json.Unmarshal([]byte(encodedIndex), &index); err != nil { - return nil, nil, fmt.Errorf("invalid image index json: %w", err) - } - - imageManifest, err = chooseBestImageManifest(architecture, &index) - if err != nil { - return nil, nil, err - } - } - } - - m, ok := imageManifest.Annotations["depot.containerimage.manifest"] - if !ok { - return nil, nil, errors.New("missing image manifest") - } - rawManifest = []byte(m) - - c, ok := imageManifest.Annotations["depot.containerimage.config"] - if !ok { - return nil, nil, errors.New("missing image config") - } - rawConfig = []byte(c) - - // Decoding both the manifest and config to ensure they are valid. - var manifest ocispecs.Manifest - if err := json.Unmarshal(rawManifest, &manifest); err != nil { - return nil, nil, fmt.Errorf("invalid image manifest json: %w", err) - } - - var image ocispecs.Image - if err := json.Unmarshal(rawConfig, &image); err != nil { - return nil, nil, fmt.Errorf("invalid image config json: %w", err) - } - return rawManifest, rawConfig, nil -} - -type RegistryProxy struct { - // ImageToPull is the image that should be pulled. - ImageToPull string - // ProxyContainerID is the ID of the container that is proxying the registry. - // Make sure to remove this container when finished. - ProxyContainerID string - - // Used to stop and remove the proxy container. - DockerAPI docker.APIClient -} - -// NewRegistryProxy creates a registry proxy that can be used to pull images from -// buildkitd cache. -// -// This also handles docker for desktop issues that prevent the registry from being -// accessed directly because the proxy is accessible by the docker daemon. -// The proxy registry translates pull requests into requests to containerd via mTLS. -// -// The running server and proxy container will be cleaned-up when Close() is called. -func NewRegistryProxy(ctx context.Context, config *ProxyConfig, dockerapi docker.APIClient) (*RegistryProxy, error) { - proxyContainer, err := RunProxyImage(ctx, dockerapi, config) - if err != nil { - return nil, err - } - - randomImageName := RandImageName() - // The tag is only for the UX during a pull. The first line will be "pulling manifest". - tag := "manifest" - // Docker is able to pull from the proxyPort on localhost. The proxy - // forwards registry requests to buildkitd via mTLS. - imageToPull := fmt.Sprintf("localhost:%s/%s:%s", proxyContainer.Port, randomImageName, tag) - - registryProxy := &RegistryProxy{ - ImageToPull: imageToPull, - ProxyContainerID: proxyContainer.ID, - DockerAPI: dockerapi, - } - - return registryProxy, nil -} - -// Close will stop and remove the registry proxy container if it was created. -func (l *RegistryProxy) Close(ctx context.Context) error { - return StopProxyContainer(ctx, l.DockerAPI, l.ProxyContainerID) -} - -// Prefer architecture, otherwise, take first available index. -func chooseBestImageManifestV2(architecture string, imageConfigs []ocispecs.Image) (int, error) { - archIdx := map[string]int{} - for i, imageConfig := range imageConfigs { - if imageConfig.Architecture == "unknown" { - continue - } - - archIdx[imageConfig.Architecture] = i - } - - // Prefer the architecture of the depot CLI host, otherwise, take first available. - if idx, ok := archIdx[architecture]; ok { - return idx, nil - } - - for _, idx := range archIdx { - return idx, nil - } - - return 0, errors.New("no manifests found") -} - -// Prefer architecture, otherwise, take first available. -func chooseBestImageManifest(architecture string, index *ocispecs.Index) (ocispecs.Descriptor, error) { - archDescriptors := map[string]ocispecs.Descriptor{} - for _, manifest := range index.Manifests { - if manifest.Platform == nil { - continue - } - - if manifest.Platform.Architecture == "unknown" { - continue - } - - archDescriptors[manifest.Platform.Architecture] = manifest - } - - // Prefer the architecture of the depot CLI host, otherwise, take first available. - if descriptor, ok := archDescriptors[architecture]; ok { - return descriptor, nil - } - - for _, descriptor := range archDescriptors { - return descriptor, nil - } - - return ocispecs.Descriptor{}, errors.New("no manifests found") -} diff --git a/pkg/load/proxy.go b/pkg/load/proxy.go deleted file mode 100644 index 87b4ee29..00000000 --- a/pkg/load/proxy.go +++ /dev/null @@ -1,155 +0,0 @@ -package load - -import ( - "context" - "encoding/base64" - "fmt" - "io" - "sync" - "time" - - "github.com/depot/cli/internal/build" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - docker "github.com/docker/docker/client" - "github.com/docker/go-connections/nat" -) - -var proxyImage = "ghcr.io/depot/cli:" + build.Version // - -type ProxyContainer struct { - ID string - Port string -} - -type ProxyConfig struct { - // Addr is the remote buildkit address (e.g. tcp://192.168.0.1) - Addr string - CACert []byte - Key []byte - Cert []byte - - // RawManifest is the raw manifest bytes for the single image to serve. - RawManifest []byte - // RawConfig is the raw config bytes for the single image to serve. - RawConfig []byte -} - -// Runs a proxy container via the docker API so that the docker daemon can pull from the local depot registry. -// This is specifically to handle docker for desktop running in a VM restricting access to the host network. -// The proxy image runs a registry proxy that connects to the remote depot buildkitd instance. -func RunProxyImage(ctx context.Context, dockerapi docker.APIClient, config *ProxyConfig) (*ProxyContainer, error) { - if err := PullProxyImage(ctx, dockerapi, proxyImage); err != nil { - return nil, err - } - - resp, err := dockerapi.ContainerCreate(ctx, - &container.Config{ - Image: proxyImage, - ExposedPorts: nat.PortSet{ - nat.Port("8888/tcp"): struct{}{}, - }, - Env: []string{ - fmt.Sprintf("CA_CERT=%s", base64.StdEncoding.EncodeToString(config.CACert)), - fmt.Sprintf("KEY=%s", base64.StdEncoding.EncodeToString(config.Key)), - fmt.Sprintf("CERT=%s", base64.StdEncoding.EncodeToString(config.Cert)), - fmt.Sprintf("ADDR=%s", base64.StdEncoding.EncodeToString([]byte(config.Addr))), - fmt.Sprintf("MANIFEST=%s", base64.StdEncoding.EncodeToString(config.RawManifest)), - fmt.Sprintf("CONFIG=%s", base64.StdEncoding.EncodeToString(config.RawConfig)), - }, - Cmd: []string{"registry"}, - Healthcheck: &container.HealthConfig{ - Test: []string{"CMD", "curl", "-f", "http://localhost:8888/v2"}, - Timeout: time.Second, - Interval: time.Second, - StartPeriod: 0, - Retries: 10, - }, - }, - &container.HostConfig{ - PublishAllPorts: true, - // This is the trick to make sure that the proxy container can - // access the host network in a cross platform way. - ExtraHosts: []string{"host.docker.internal:host-gateway"}, - }, - nil, - nil, - fmt.Sprintf("depot-registry-proxy-%s", RandImageName()), // unique container name - ) - - if err != nil { - return nil, err - } - - if err := dockerapi.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - return nil, err - } - - for retries := 0; retries < 10; retries++ { - inspect, err := dockerapi.ContainerInspect(ctx, resp.ID) - if err != nil { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - _ = StopProxyContainer(ctx, dockerapi, resp.ID) - return nil, err - } - - if inspect.State.Health != nil && inspect.State.Health.Status == "healthy" { - binds := inspect.NetworkSettings.Ports[nat.Port("8888/tcp")] - var proxyPortOnHost string - for _, bind := range binds { - proxyPortOnHost = bind.HostPort - } - - return &ProxyContainer{ - ID: resp.ID, - Port: proxyPortOnHost, - }, nil - } - - time.Sleep(1 * time.Second) - } - - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - _ = StopProxyContainer(ctx, dockerapi, resp.ID) - return nil, fmt.Errorf("timed out waiting for registry to be ready") -} - -var ( - downloadedProxyImage sync.Once - downloadProxyImageErr error -) - -// PullProxyImage will pull the proxy image into docker. -// This is done once per process as a performance optimization. -// Additionally, if the proxy image is already present, this will not pull the image. -func PullProxyImage(ctx context.Context, dockerapi docker.APIClient, imageName string) error { - downloadedProxyImage.Do(func() { - // Check if image already has been downloaded. - images, err := dockerapi.ImageList(ctx, types.ImageListOptions{ - Filters: filters.NewArgs(filters.Arg("reference", imageName)), - }) - - // Any error or no matching images means we need to pull the image. - // The goal is to save about a second or two of startup time. - if err != nil || len(images) == 0 { - var body io.ReadCloser - body, downloadProxyImageErr = dockerapi.ImagePull(ctx, imageName, types.ImagePullOptions{}) - if downloadProxyImageErr != nil { - return - } - defer func() { _ = body.Close() }() - _, downloadProxyImageErr = io.Copy(io.Discard, body) - return - } - }) - - return downloadProxyImageErr -} - -// Forcefully stops and removes the proxy container. -func StopProxyContainer(ctx context.Context, dockerapi docker.APIClient, containerID string) error { - return dockerapi.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{Force: true, RemoveVolumes: true}) -}