diff --git a/docker/inspect.go b/docker/inspect.go new file mode 100644 index 00000000..d33e05d5 --- /dev/null +++ b/docker/inspect.go @@ -0,0 +1,293 @@ +package docker + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/docker/distribution/digest" + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/image" + versionPkg "github.com/docker/docker/pkg/version" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + engineTypes "github.com/docker/engine-api/types" + registryTypes "github.com/docker/engine-api/types/registry" + "github.com/runcom/skopeo/types" + "golang.org/x/net/context" +) + +// fallbackError wraps an error that can possibly allow fallback to a different +// endpoint. +type fallbackError struct { + // err is the error being wrapped. + err error + // confirmedV2 is set to true if it was confirmed that the registry + // supports the v2 protocol. This is used to limit fallbacks to the v1 + // protocol. + confirmedV2 bool +} + +// Error renders the FallbackError as a string. +func (f fallbackError) Error() string { + return f.err.Error() +} + +type manifestFetcher interface { + Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) +} + +func validateName(name string) error { + distref, err := distreference.ParseNamed(name) + if err != nil { + return err + } + hostname, _ := distreference.SplitHostname(distref) + if hostname == "" { + return fmt.Errorf("Please use a fully qualified repository name") + } + return nil +} + +func GetData(c *cli.Context, name string) (*types.ImageInspect, error) { + if err := validateName(name); err != nil { + return nil, err + } + ref, err := reference.ParseNamed(name) + if err != nil { + return nil, err + } + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + authConfig, err := getAuthConfig(c, repoInfo.Index) + if err != nil { + return nil, err + } + if err := validateRepoName(repoInfo.Name()); err != nil { + return nil, err + } + //options := ®istry.Options{} + //options.Mirrors = opts.NewListOpts(nil) + //options.InsecureRegistries = opts.NewListOpts(nil) + //options.InsecureRegistries.Set("0.0.0.0/0") + //registryService := registry.NewService(options) + registryService := registry.NewService(nil) + //// TODO(runcom): hacky, provide a way of passing tls cert (flag?) to be used to lookup + //for _, ic := range registryService.Config.IndexConfigs { + //ic.Secure = false + //} + + endpoints, err := registryService.LookupPullEndpoints(repoInfo) + if err != nil { + return nil, err + } + + var ( + ctx = context.Background() + lastErr error + discardNoSupportErrors bool + imgInspect *types.ImageInspect + confirmedV2 bool + ) + + for _, endpoint := range endpoints { + // make sure I can reach the registry, same as docker pull does + v1endpoint, err := endpoint.ToV1Endpoint(nil) + if err != nil { + return nil, err + } + if _, err := v1endpoint.Ping(); err != nil { + if strings.Contains(err.Error(), "timeout") { + return nil, err + } + continue + } + + if confirmedV2 && endpoint.Version == registry.APIVersion1 { + logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) + continue + } + logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version) + + //fetcher, err := newManifestFetcher(endpoint, repoInfo, config) + fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService) + if err != nil { + lastErr = err + continue + } + + if imgInspect, err = fetcher.Fetch(ctx, ref); err != nil { + // Was this fetch cancelled? If so, don't try to fall back. + fallback := false + select { + case <-ctx.Done(): + default: + if fallbackErr, ok := err.(fallbackError); ok { + fallback = true + confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 + err = fallbackErr.err + } + } + if fallback { + if _, ok := err.(registry.ErrNoSupport); !ok { + // Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors. + discardNoSupportErrors = true + // save the current error + lastErr = err + } else if !discardNoSupportErrors { + // Save the ErrNoSupport error, because it's either the first error or all encountered errors + // were also ErrNoSupport errors. + lastErr = err + } + continue + } + logrus.Debugf("Not continuing with error: %v", err) + return nil, err + } + + return imgInspect, nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no endpoints found for %s", ref.String()) + } + + return nil, lastErr +} + +func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig engineTypes.AuthConfig, registryService *registry.Service) (manifestFetcher, error) { + switch endpoint.Version { + case registry.APIVersion2: + return &v2ManifestFetcher{ + endpoint: endpoint, + authConfig: authConfig, + service: registryService, + repoInfo: repoInfo, + }, nil + case registry.APIVersion1: + return &v1ManifestFetcher{ + endpoint: endpoint, + authConfig: authConfig, + service: registryService, + repoInfo: repoInfo, + }, nil + } + return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL) +} + +func getAuthConfig(c *cli.Context, index *registryTypes.IndexInfo) (engineTypes.AuthConfig, error) { + var ( + username = c.GlobalString("username") + password = c.GlobalString("password") + cfg = c.GlobalString("docker-cfg") + defAuthConfig = engineTypes.AuthConfig{ + Username: c.GlobalString("username"), + Password: c.GlobalString("password"), + Email: "stub@example.com", + } + ) + + // TODO(runcom): implement this to opt-in for docker-cfg, no need to make this + // work by default with docker's conf + //useDockerConf := c.GlobalString("use-docker-cfg") + + if username != "" && password != "" { + return defAuthConfig, nil + } + + if _, err := os.Stat(cfg); err != nil { + logrus.Debugf("Docker cli config file %q not found: %v, falling back to --username and --password if needed", cfg, err) + if os.IsNotExist(err) { + return defAuthConfig, nil + } + return engineTypes.AuthConfig{}, nil + } + confFile, err := cliconfig.Load(cfg) + if err != nil { + return engineTypes.AuthConfig{}, err + } + authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index) + logrus.Debugf("authConfig for %s: %v", index.Name, authConfig) + + return authConfig, nil +} + +func validateRepoName(name string) error { + if name == "" { + return fmt.Errorf("Repository name can't be empty") + } + if name == api.NoBaseImageSpecifier { + return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier) + } + return nil +} + +func makeImageInspect(img *image.Image, tag string, dgst digest.Digest, tagList []string) *types.ImageInspect { + var digest string + if err := dgst.Validate(); err == nil { + digest = dgst.String() + } + return &types.ImageInspect{ + Tag: tag, + Digest: digest, + RepoTags: tagList, + Comment: img.Comment, + Created: img.Created.Format(time.RFC3339Nano), + ContainerConfig: &img.ContainerConfig, + DockerVersion: img.DockerVersion, + Author: img.Author, + Config: img.Config, + Architecture: img.Architecture, + Os: img.OS, + } +} + +func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) { + var dver struct { + DockerVersion string `json:"docker_version"` + } + + if err := json.Unmarshal(imageJSON, &dver); err != nil { + return nil, err + } + + useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3") + + if useFallback { + var v1Image image.V1Image + err := json.Unmarshal(imageJSON, &v1Image) + if err != nil { + return nil, err + } + imageJSON, err = json.Marshal(v1Image) + if err != nil { + return nil, err + } + } + + var c map[string]*json.RawMessage + if err := json.Unmarshal(imageJSON, &c); err != nil { + return nil, err + } + + c["rootfs"] = rawJSON(rootfs) + c["history"] = rawJSON(history) + + return c, nil +} + +func rawJSON(value interface{}) *json.RawMessage { + jsonval, err := json.Marshal(value) + if err != nil { + return nil + } + return (*json.RawMessage)(&jsonval) +} diff --git a/inspect_v1.go b/docker/inspect_v1.go similarity index 94% rename from inspect_v1.go rename to docker/inspect_v1.go index 6ec4a600..591ca212 100644 --- a/inspect_v1.go +++ b/docker/inspect_v1.go @@ -1,4 +1,4 @@ -package main +package docker import ( "encoding/json" @@ -13,7 +13,8 @@ import ( "github.com/docker/docker/image/v1" "github.com/docker/docker/reference" "github.com/docker/docker/registry" - "github.com/docker/engine-api/types" + engineTypes "github.com/docker/engine-api/types" + "github.com/runcom/skopeo/types" "golang.org/x/net/context" ) @@ -23,14 +24,14 @@ type v1ManifestFetcher struct { repo distribution.Repository confirmedV2 bool // wrap in a config? - authConfig types.AuthConfig + authConfig engineTypes.AuthConfig service *registry.Service session *registry.Session } -func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) { +func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) { var ( - imgInspect *imageInspect + imgInspect *types.ImageInspect ) if _, isCanonical := ref.(reference.Canonical); isCanonical { // Allowing fallback, because HTTPS v1 is before HTTP v2 @@ -65,7 +66,7 @@ func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i return imgInspect, nil } -func (mf *v1ManifestFetcher) fetchWithSession(ctx context.Context, ref reference.Named) (*imageInspect, error) { +func (mf *v1ManifestFetcher) fetchWithSession(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) { repoData, err := mf.session.GetRepositoryData(mf.repoInfo) if err != nil { if strings.Contains(err.Error(), "HTTP code: 404") { diff --git a/inspect_v2.go b/docker/inspect_v2.go similarity index 98% rename from inspect_v2.go rename to docker/inspect_v2.go index bd25f0fb..10856075 100644 --- a/inspect_v2.go +++ b/docker/inspect_v2.go @@ -1,4 +1,4 @@ -package main +package docker import ( "encoding/json" @@ -19,7 +19,8 @@ import ( "github.com/docker/docker/image/v1" "github.com/docker/docker/reference" "github.com/docker/docker/registry" - "github.com/docker/engine-api/types" + engineTypes "github.com/docker/engine-api/types" + "github.com/runcom/skopeo/types" "golang.org/x/net/context" ) @@ -29,13 +30,13 @@ type v2ManifestFetcher struct { repo distribution.Repository confirmedV2 bool // wrap in a config? - authConfig types.AuthConfig + authConfig engineTypes.AuthConfig service *registry.Service } -func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) { +func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) { var ( - imgInspect *imageInspect + imgInspect *types.ImageInspect err error ) @@ -58,7 +59,7 @@ func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i return imgInspect, err } -func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) (*imageInspect, error) { +func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) (*types.ImageInspect, error) { var ( manifest distribution.Manifest tagOrDigest string // Used for logging/progress only diff --git a/inspect.go b/inspect.go index d155f0bd..f4aaba03 100644 --- a/inspect.go +++ b/inspect.go @@ -1,316 +1,30 @@ package main import ( - "encoding/json" "fmt" - "os" - "strings" - "time" - "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" - "github.com/docker/distribution/digest" - distreference "github.com/docker/distribution/reference" - "github.com/docker/docker/api" - "github.com/docker/docker/cliconfig" - "github.com/docker/docker/image" - versionPkg "github.com/docker/docker/pkg/version" - "github.com/docker/docker/reference" - "github.com/docker/docker/registry" - types "github.com/docker/engine-api/types" - containerTypes "github.com/docker/engine-api/types/container" - registryTypes "github.com/docker/engine-api/types/registry" - "golang.org/x/net/context" + "github.com/runcom/skopeo/docker" + "github.com/runcom/skopeo/types" ) -// fallbackError wraps an error that can possibly allow fallback to a different -// endpoint. -type fallbackError struct { - // err is the error being wrapped. - err error - // confirmedV2 is set to true if it was confirmed that the registry - // supports the v2 protocol. This is used to limit fallbacks to the v1 - // protocol. - confirmedV2 bool -} - -// Error renders the FallbackError as a string. -func (f fallbackError) Error() string { - return f.err.Error() -} - -type manifestFetcher interface { - Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) -} - -type imageInspect struct { - Tag string - Digest string - RepoTags []string - Comment string - Created string - ContainerConfig *containerTypes.Config - DockerVersion string - Author string - Config *containerTypes.Config - Architecture string - Os string -} - -func validateName(name string) error { - distref, err := distreference.ParseNamed(name) - if err != nil { - return err - } - hostname, _ := distreference.SplitHostname(distref) - if hostname == "" { - return fmt.Errorf("Please use a fully qualified repository name") - } - return nil -} - -func inspect(c *cli.Context) (*imageInspect, error) { - name := c.Args().First() - if err := validateName(name); err != nil { - return nil, err - } - ref, err := reference.ParseNamed(name) - if err != nil { - return nil, err - } - imgInspect, err := getData(c, ref) - if err != nil { - return nil, err +func inspect(c *cli.Context) (*types.ImageInspect, error) { + var ( + imgInspect *types.ImageInspect + err error + name = c.Args().First() + imgType = c.GlobalString("img-type") + ) + switch imgType { + case "docker": + imgInspect, err = docker.GetData(c, name) + if err != nil { + return nil, err + } + case "appc": + return nil, fmt.Errorf("TODO") + default: + return nil, fmt.Errorf("%s image type is invalid, please use either 'docker' or 'appc'", imgType) } return imgInspect, nil } - -func getData(c *cli.Context, ref reference.Named) (*imageInspect, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - authConfig, err := getAuthConfig(c, repoInfo.Index) - if err != nil { - return nil, err - } - if err := validateRepoName(repoInfo.Name()); err != nil { - return nil, err - } - //options := ®istry.Options{} - //options.Mirrors = opts.NewListOpts(nil) - //options.InsecureRegistries = opts.NewListOpts(nil) - //options.InsecureRegistries.Set("0.0.0.0/0") - //registryService := registry.NewService(options) - registryService := registry.NewService(nil) - //// TODO(runcom): hacky, provide a way of passing tls cert (flag?) to be used to lookup - //for _, ic := range registryService.Config.IndexConfigs { - //ic.Secure = false - //} - - endpoints, err := registryService.LookupPullEndpoints(repoInfo) - if err != nil { - return nil, err - } - - var ( - ctx = context.Background() - lastErr error - discardNoSupportErrors bool - imgInspect *imageInspect - confirmedV2 bool - ) - - for _, endpoint := range endpoints { - // make sure I can reach the registry, same as docker pull does - v1endpoint, err := endpoint.ToV1Endpoint(nil) - if err != nil { - return nil, err - } - if _, err := v1endpoint.Ping(); err != nil { - if strings.Contains(err.Error(), "timeout") { - return nil, err - } - continue - } - - if confirmedV2 && endpoint.Version == registry.APIVersion1 { - logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL) - continue - } - logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name(), endpoint.URL, endpoint.Version) - - //fetcher, err := newManifestFetcher(endpoint, repoInfo, config) - fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService) - if err != nil { - lastErr = err - continue - } - - if imgInspect, err = fetcher.Fetch(ctx, ref); err != nil { - // Was this fetch cancelled? If so, don't try to fall back. - fallback := false - select { - case <-ctx.Done(): - default: - if fallbackErr, ok := err.(fallbackError); ok { - fallback = true - confirmedV2 = confirmedV2 || fallbackErr.confirmedV2 - err = fallbackErr.err - } - } - if fallback { - if _, ok := err.(registry.ErrNoSupport); !ok { - // Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors. - discardNoSupportErrors = true - // save the current error - lastErr = err - } else if !discardNoSupportErrors { - // Save the ErrNoSupport error, because it's either the first error or all encountered errors - // were also ErrNoSupport errors. - lastErr = err - } - continue - } - logrus.Debugf("Not continuing with error: %v", err) - return nil, err - } - - return imgInspect, nil - } - - if lastErr == nil { - lastErr = fmt.Errorf("no endpoints found for %s", ref.String()) - } - - return nil, lastErr -} - -func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, registryService *registry.Service) (manifestFetcher, error) { - switch endpoint.Version { - case registry.APIVersion2: - return &v2ManifestFetcher{ - endpoint: endpoint, - authConfig: authConfig, - service: registryService, - repoInfo: repoInfo, - }, nil - case registry.APIVersion1: - return &v1ManifestFetcher{ - endpoint: endpoint, - authConfig: authConfig, - service: registryService, - repoInfo: repoInfo, - }, nil - } - return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL) -} - -func getAuthConfig(c *cli.Context, index *registryTypes.IndexInfo) (types.AuthConfig, error) { - var ( - username = c.GlobalString("username") - password = c.GlobalString("password") - cfg = c.GlobalString("docker-cfg") - defAuthConfig = types.AuthConfig{ - Username: c.GlobalString("username"), - Password: c.GlobalString("password"), - Email: "stub@example.com", - } - ) - - // TODO(runcom): implement this to opt-in for docker-cfg, no need to make this - // work by default with docker's conf - //useDockerConf := c.GlobalString("use-docker-cfg") - - if username != "" && password != "" { - return defAuthConfig, nil - } - - if _, err := os.Stat(cfg); err != nil { - logrus.Debugf("Docker cli config file %q not found: %v, falling back to --username and --password if needed", cfg, err) - if os.IsNotExist(err) { - return defAuthConfig, nil - } - return types.AuthConfig{}, nil - } - confFile, err := cliconfig.Load(cfg) - if err != nil { - return types.AuthConfig{}, err - } - authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index) - logrus.Debugf("authConfig for %s: %v", index.Name, authConfig) - - return authConfig, nil -} - -func validateRepoName(name string) error { - if name == "" { - return fmt.Errorf("Repository name can't be empty") - } - if name == api.NoBaseImageSpecifier { - return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier) - } - return nil -} - -func makeImageInspect(img *image.Image, tag string, dgst digest.Digest, tagList []string) *imageInspect { - var digest string - if err := dgst.Validate(); err == nil { - digest = dgst.String() - } - return &imageInspect{ - Tag: tag, - Digest: digest, - RepoTags: tagList, - Comment: img.Comment, - Created: img.Created.Format(time.RFC3339Nano), - ContainerConfig: &img.ContainerConfig, - DockerVersion: img.DockerVersion, - Author: img.Author, - Config: img.Config, - Architecture: img.Architecture, - Os: img.OS, - } -} - -func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) { - var dver struct { - DockerVersion string `json:"docker_version"` - } - - if err := json.Unmarshal(imageJSON, &dver); err != nil { - return nil, err - } - - useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3") - - if useFallback { - var v1Image image.V1Image - err := json.Unmarshal(imageJSON, &v1Image) - if err != nil { - return nil, err - } - imageJSON, err = json.Marshal(v1Image) - if err != nil { - return nil, err - } - } - - var c map[string]*json.RawMessage - if err := json.Unmarshal(imageJSON, &c); err != nil { - return nil, err - } - - c["rootfs"] = rawJSON(rootfs) - c["history"] = rawJSON(history) - - return c, nil -} - -func rawJSON(value interface{}) *json.RawMessage { - jsonval, err := json.Marshal(value) - if err != nil { - return nil - } - return (*json.RawMessage)(&jsonval) -} diff --git a/main.go b/main.go index 082af9e7..07a7b887 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,11 @@ func main() { Value: cliconfig.ConfigDir(), Usage: "Docker's cli config for auth", }, + cli.StringFlag{ + Name: "img-type", + Value: "", + Usage: "Either docker or appc", + }, } app.Before = func(c *cli.Context) error { if c.GlobalBool("debug") { diff --git a/types/types.go b/types/types.go new file mode 100644 index 00000000..7f040fe5 --- /dev/null +++ b/types/types.go @@ -0,0 +1,19 @@ +package types + +import ( + containerTypes "github.com/docker/engine-api/types/container" +) + +type ImageInspect struct { + Tag string + Digest string + RepoTags []string + Comment string + Created string + ContainerConfig *containerTypes.Config + DockerVersion string + Author string + Config *containerTypes.Config + Architecture string + Os string +}