GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

client.go 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package registry
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "net/http"
  7. "reflect"
  8. "time"
  9. "github.com/docker/distribution"
  10. "github.com/docker/distribution/manifest/manifestlist"
  11. "github.com/docker/distribution/manifest/schema1"
  12. "github.com/docker/distribution/manifest/schema2"
  13. "github.com/docker/distribution/registry/client"
  14. "github.com/opencontainers/go-digest"
  15. "github.com/weaveworks/flux/image"
  16. )
  17. type Excluded struct {
  18. ExcludedReason string `json:",omitempty"`
  19. }
  20. // ImageEntry represents a result from looking up an image ref in an
  21. // image registry. It's an either-or: either you get an image.Info, or
  22. // you get a reason that the image should be treated as unusable
  23. // (e.g., it's for the wrong architecture).
  24. type ImageEntry struct {
  25. image.Info `json:",omitempty"`
  26. Excluded
  27. }
  28. // MarshalJSON does custom JSON marshalling for ImageEntry values. We
  29. // need this because the struct embeds the image.Info type, which has
  30. // its own custom marshaling, which would get used otherwise.
  31. func (entry ImageEntry) MarshalJSON() ([]byte, error) {
  32. // We can only do it this way because it's explicitly an either-or
  33. // -- I don't know of a way to inline all the fields when one of
  34. // the things you're inlining defines its own MarshalJSON.
  35. if entry.ExcludedReason != "" {
  36. return json.Marshal(entry.Excluded)
  37. }
  38. return json.Marshal(entry.Info)
  39. }
  40. // UnmarshalJSON does custom JSON unmarshalling for ImageEntry values.
  41. func (entry *ImageEntry) UnmarshalJSON(bytes []byte) error {
  42. if err := json.Unmarshal(bytes, &entry.Info); err != nil {
  43. return err
  44. }
  45. if err := json.Unmarshal(bytes, &entry.Excluded); err != nil {
  46. return err
  47. }
  48. return nil
  49. }
  50. // Client is a remote registry client for a particular image
  51. // repository (e.g., for docker.io/weaveworks/flux). It is an interface
  52. // so we can wrap it in instrumentation, write fake implementations,
  53. // and so on.
  54. type Client interface {
  55. Tags(context.Context) ([]string, error)
  56. Manifest(ctx context.Context, ref string) (ImageEntry, error)
  57. }
  58. // ClientFactory supplies Client implementations for a given repo,
  59. // with credentials. This is an interface so we can provide fake
  60. // implementations.
  61. type ClientFactory interface {
  62. ClientFor(image.CanonicalName, Credentials) (Client, error)
  63. Succeed(image.CanonicalName)
  64. }
  65. type Remote struct {
  66. transport http.RoundTripper
  67. repo image.CanonicalName
  68. base string
  69. }
  70. // Adapt to docker distribution `reference.Named`.
  71. type named struct {
  72. image.CanonicalName
  73. }
  74. // Name returns the name of the repository. These values are used by
  75. // the docker distribution client package to build API URLs, and (it
  76. // turns out) are _not_ expected to include a domain (e.g.,
  77. // quay.io). Hence, the implementation here just returns the path.
  78. func (n named) Name() string {
  79. return n.Image
  80. }
  81. // Return the tags for this repository.
  82. func (a *Remote) Tags(ctx context.Context) ([]string, error) {
  83. repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
  84. if err != nil {
  85. return nil, err
  86. }
  87. return repository.Tags(ctx).All(ctx)
  88. }
  89. // Manifest fetches the metadata for an image reference; currently
  90. // assumed to be in the same repo as that provided to `NewRemote(...)`
  91. func (a *Remote) Manifest(ctx context.Context, ref string) (ImageEntry, error) {
  92. repository, err := client.NewRepository(named{a.repo}, a.base, a.transport)
  93. if err != nil {
  94. return ImageEntry{}, err
  95. }
  96. manifests, err := repository.Manifests(ctx)
  97. if err != nil {
  98. return ImageEntry{}, err
  99. }
  100. var manifestDigest digest.Digest
  101. digestOpt := client.ReturnContentDigest(&manifestDigest)
  102. manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), digestOpt, distribution.WithTagOption{ref})
  103. interpret:
  104. if fetchErr != nil {
  105. return ImageEntry{}, fetchErr
  106. }
  107. info := image.Info{ID: a.repo.ToRef(ref), Digest: manifestDigest.String()}
  108. // TODO(michael): can we type switch? Not sure how dependable the
  109. // underlying types are.
  110. switch deserialised := manifest.(type) {
  111. case *schema1.SignedManifest:
  112. var man schema1.Manifest = deserialised.Manifest
  113. // for decoding the v1-compatibility entry in schema1 manifests
  114. var v1 struct {
  115. ID string `json:"id"`
  116. Created time.Time `json:"created"`
  117. OS string `json:"os"`
  118. Arch string `json:"architecture"`
  119. }
  120. if err = json.Unmarshal([]byte(man.History[0].V1Compatibility), &v1); err != nil {
  121. return ImageEntry{}, err
  122. }
  123. // This is not the ImageID that Docker uses, but assumed to
  124. // identify the image as it's the topmost layer.
  125. info.ImageID = v1.ID
  126. info.CreatedAt = v1.Created
  127. case *schema2.DeserializedManifest:
  128. var man schema2.Manifest = deserialised.Manifest
  129. configBytes, err := repository.Blobs(ctx).Get(ctx, man.Config.Digest)
  130. if err != nil {
  131. return ImageEntry{}, err
  132. }
  133. var config struct {
  134. Arch string `json:"architecture"`
  135. Created time.Time `json:"created"`
  136. OS string `json:"os"`
  137. }
  138. if err = json.Unmarshal(configBytes, &config); err != nil {
  139. return ImageEntry{}, err
  140. }
  141. // This _is_ what Docker uses as its Image ID.
  142. info.ImageID = man.Config.Digest.String()
  143. info.CreatedAt = config.Created
  144. case *manifestlist.DeserializedManifestList:
  145. var list manifestlist.ManifestList = deserialised.ManifestList
  146. // TODO(michael): is it valid to just pick the first one that matches?
  147. for _, m := range list.Manifests {
  148. if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
  149. manifest, fetchErr = manifests.Get(ctx, m.Digest, digestOpt)
  150. goto interpret
  151. }
  152. }
  153. entry := ImageEntry{}
  154. entry.ExcludedReason = "no suitable manifest (linux amd64) in manifestlist"
  155. return entry, nil
  156. default:
  157. t := reflect.TypeOf(manifest)
  158. return ImageEntry{}, errors.New("unknown manifest type: " + t.String())
  159. }
  160. return ImageEntry{Info: info}, nil
  161. }