github.com/simonferquel/app@v0.6.1-0.20181012141724-68b7cccf26ac/pkg/resto/resto.go (about) 1 package resto 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "compress/gzip" 7 "context" 8 "encoding/json" 9 "fmt" 10 "io" 11 "strings" 12 "time" 13 14 "github.com/docker/cli/cli/config" 15 "github.com/docker/distribution" 16 "github.com/docker/distribution/manifest" 17 "github.com/docker/distribution/manifest/schema2" 18 "github.com/docker/distribution/reference" 19 "github.com/docker/distribution/registry/client" 20 digest "github.com/opencontainers/go-digest" 21 ociv1 "github.com/opencontainers/image-spec/specs-go/v1" 22 log "github.com/sirupsen/logrus" 23 ) 24 25 // RegistryOptions contains optional configuration for Registry operations 26 type RegistryOptions struct { 27 Username string 28 Password string 29 Insecure bool 30 CleartextCredentials bool 31 } 32 33 type unsupportedMediaType struct{} 34 35 func (u unsupportedMediaType) Error() string { 36 return "Unsupported media type" 37 } 38 39 // ManifestAny is a manifest type for arbitrary configuration data 40 type ManifestAny struct { 41 manifest.Versioned 42 Payload string `json:"payload,omitempty"` 43 } 44 45 type parsedReference struct { 46 domain string 47 path string 48 tag string 49 } 50 51 func parseRef(repoTag string) (parsedReference, error) { 52 rawref, err := reference.ParseNormalizedNamed(repoTag) 53 if err != nil { 54 return parsedReference{}, err 55 } 56 ref, ok := rawref.(reference.Named) 57 if !ok { 58 return parseRef("docker.io/" + repoTag) 59 } 60 tag := "latest" 61 if rt, ok := ref.(reference.Tagged); ok { 62 tag = rt.Tag() 63 } 64 domain := reference.Domain(ref) 65 if domain == "docker.io" { 66 domain = "registry-1.docker.io" 67 } 68 return parsedReference{"https://" + domain, reference.Path(ref), tag}, nil 69 } 70 71 func getCredentials(domain string) (string, string, error) { 72 cfg, err := config.Load("") 73 if err != nil { 74 return "", "", err 75 } 76 switch domain { 77 case "https://registry-1.docker.io": 78 domain = "https://index.docker.io/v1/" 79 default: 80 domain = strings.TrimPrefix(domain, "https://") 81 } 82 auth, err := cfg.GetAuthConfig(domain) 83 if err != nil { 84 return "", "", err 85 } 86 return auth.Username, auth.Password, nil 87 } 88 89 func makeTarGz(content map[string]string) ([]byte, digest.Digest, error) { 90 buf := bytes.NewBuffer(nil) 91 err := func() error { 92 w := tar.NewWriter(buf) 93 defer w.Close() 94 for k, v := range content { 95 if err := w.WriteHeader(&tar.Header{ 96 Typeflag: tar.TypeReg, 97 Name: k, 98 Mode: 0600, 99 Size: int64(len(v)), 100 }); err != nil { 101 return err 102 } 103 if _, err := w.Write([]byte(v)); err != nil { 104 return err 105 } 106 } 107 return nil 108 }() 109 if err != nil { 110 return nil, "", err 111 } 112 dgst := digest.SHA256.FromBytes(buf.Bytes()) 113 gzbuf := bytes.NewBuffer(nil) 114 g := gzip.NewWriter(gzbuf) 115 if _, err := g.Write(buf.Bytes()); err != nil { 116 return nil, "", err 117 } 118 if err := g.Close(); err != nil { 119 return nil, "", err 120 } 121 return gzbuf.Bytes(), dgst, nil 122 } 123 124 const maxRepositoryCount = 10000 125 126 // ListRepositories lists all the repositories in a registry 127 func ListRepositories(ctx context.Context, endpoint string, opts RegistryOptions) ([]string, error) { 128 tr, err := NewTransportCatalog(endpoint, opts) 129 if err != nil { 130 return nil, err 131 } 132 registry, err := client.NewRegistry(endpoint, tr) 133 if err != nil { 134 return nil, err 135 } 136 entries := make([]string, maxRepositoryCount) 137 count, err := registry.Repositories(ctx, entries, "") 138 if err != nil && err != io.EOF { 139 return nil, err 140 } 141 return entries[0:count], nil 142 } 143 144 // ListTags lists all the tags in a repository 145 func ListTags(ctx context.Context, reponame string, opts RegistryOptions) ([]string, error) { 146 pr, err := parseRef(reponame) 147 if err != nil { 148 return nil, err 149 } 150 repo, err := NewRepository(ctx, pr.domain, pr.path, opts) 151 if err != nil { 152 return nil, err 153 } 154 tagService := repo.Tags(ctx) 155 return tagService.All(ctx) 156 } 157 158 // PullConfig pulls a configuration file from a registry 159 func PullConfig(ctx context.Context, repoTag string, opts RegistryOptions) (string, error) { 160 res, err := PullConfigMulti(ctx, repoTag, opts) 161 if err != nil { 162 return "", err 163 } 164 return res["config"], nil 165 } 166 167 // PullConfigMulti pulls a set of configuration files from a registry 168 func PullConfigMulti(ctx context.Context, repoTag string, opts RegistryOptions) (map[string]string, error) { 169 pr, err := parseRef(repoTag) 170 if err != nil { 171 return nil, err 172 } 173 if opts.Username == "" { 174 opts.Username, opts.Password, err = getCredentials(pr.domain) 175 if err != nil { 176 log.Debugf("failed to get credentials for %s: %s", pr.domain, err) 177 } 178 } 179 repo, err := NewRepository(ctx, pr.domain, pr.path, opts) 180 if err != nil { 181 return nil, err 182 } 183 tagService := repo.Tags(ctx) 184 dgst, err := tagService.Get(ctx, pr.tag) 185 if err != nil { 186 return nil, err 187 } 188 manifestService, err := repo.Manifests(ctx) 189 if err != nil { 190 return nil, err 191 } 192 manifest, err := manifestService.Get(ctx, dgst.Digest) 193 if err != nil { 194 return nil, err 195 } 196 mediaType, payload, err := manifest.Payload() 197 if err != nil { 198 return nil, err 199 } 200 if mediaType == MediaTypeConfig { 201 var ma ManifestAny 202 if err := json.Unmarshal(payload, &ma); err != nil { 203 return nil, err 204 } 205 res := make(map[string]string) 206 err = json.Unmarshal([]byte(ma.Payload), &res) 207 return res, err 208 } 209 // legacy image mode 210 return pullConfigImage(ctx, manifest, repo) 211 } 212 213 func pullConfigImage(ctx context.Context, manifest distribution.Manifest, repo distribution.Repository) (map[string]string, error) { 214 refs := manifest.References() 215 if len(refs) != 2 { 216 return nil, fmt.Errorf("expected 2 references, found %v", len(refs)) 217 } 218 // assume second element is the layer (first being the image config) 219 r := refs[1] 220 rdgst := r.Digest 221 blobsService := repo.Blobs(ctx) 222 payloadGz, err := blobsService.Get(ctx, rdgst) 223 if err != nil { 224 return nil, err 225 } 226 payloadBuf := bytes.NewBuffer(payloadGz) 227 gzf, err := gzip.NewReader(payloadBuf) 228 if err != nil { 229 return nil, err 230 } 231 tarReader := tar.NewReader(gzf) 232 return tarContent(tarReader) 233 } 234 235 func tarContent(tarReader *tar.Reader) (map[string]string, error) { 236 res := make(map[string]string) 237 for { 238 header, err := tarReader.Next() 239 if err == io.EOF { 240 break 241 } 242 if err != nil { 243 return res, err 244 } 245 if header.Typeflag == tar.TypeReg { 246 content := bytes.NewBuffer(nil) 247 io.Copy(content, tarReader) 248 res[header.Name] = content.String() 249 } 250 } 251 return res, nil 252 } 253 254 // PushConfig pushes a configuration file to a registry and returns its digest 255 func PushConfig(ctx context.Context, payload, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) { 256 return PushConfigMulti(ctx, map[string]string{ 257 "config": payload, 258 }, repoTag, opts, labels) 259 } 260 261 // PushConfigMulti pushes a set of configuration files to a registry and returns its digest 262 func PushConfigMulti(ctx context.Context, payload map[string]string, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) { 263 pr, err := parseRef(repoTag) 264 if err != nil { 265 return "", err 266 } 267 if opts.Username == "" { 268 opts.Username, opts.Password, err = getCredentials(pr.domain) 269 if err != nil { 270 log.Debugf("failed to get credentials for %s: %s", pr.domain, err) 271 } 272 } 273 repo, err := NewRepository(ctx, pr.domain, pr.path, opts) 274 if err != nil { 275 return "", err 276 } 277 digest, err := pushConfigMediaType(ctx, payload, pr, repo) 278 if err == nil { 279 return digest, err 280 } 281 if _, ok := err.(unsupportedMediaType); ok { 282 return pushConfigLegacy(ctx, payload, pr, repo, labels) 283 } 284 return digest, err 285 } 286 287 func pushConfigMediaType(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository) (string, error) { 288 j, err := json.Marshal(payload) 289 if err != nil { 290 return "", err 291 } 292 manifestAny := ManifestAny{ 293 Versioned: manifest.Versioned{ 294 SchemaVersion: 2, 295 MediaType: MediaTypeConfig, 296 }, 297 Payload: string(j), 298 } 299 raw, err := json.Marshal(manifestAny) 300 if err != nil { 301 return "", err 302 } 303 manifestService, err := repo.Manifests(ctx) 304 if err != nil { 305 return "", err 306 } 307 manifest := NewConfigManifest(MediaTypeConfig, raw) 308 dgst, err := manifestService.Put(ctx, manifest, distribution.WithTag(pr.tag)) 309 if err == nil { 310 return dgst.String(), nil 311 } 312 switch { 313 case strings.Contains(err.Error(), "manifest invalid"): 314 return "", unsupportedMediaType{} 315 case strings.Contains(err.Error(), "manifest Unknown"): 316 return "", unsupportedMediaType{} 317 default: 318 return "", err 319 } 320 } 321 322 func pushConfigLegacy(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository, labels map[string]string) (string, error) { 323 manifestService, err := repo.Manifests(ctx) 324 if err != nil { 325 return "", err 326 } 327 // try legacy mode 328 // create payload 329 payloadGz, payloadUncompressedDigest, err := makeTarGz(payload) 330 if err != nil { 331 return "", err 332 } 333 blobsService := repo.Blobs(ctx) 334 payloadDesc, err := blobsService.Put(ctx, schema2.MediaTypeLayer, payloadGz) 335 if err != nil { 336 return "", err 337 } 338 payloadDesc.MediaType = schema2.MediaTypeLayer 339 // create dummy image config 340 now := time.Now() 341 imageConfig := ociv1.Image{ 342 Created: &now, 343 Architecture: "config", 344 OS: "config", 345 Config: ociv1.ImageConfig{ 346 Labels: labels, 347 }, 348 RootFS: ociv1.RootFS{ 349 Type: "layers", 350 DiffIDs: []digest.Digest{payloadUncompressedDigest}, 351 }, 352 History: []ociv1.History{ 353 {CreatedBy: "COPY configfile /"}, 354 }, 355 } 356 icm, err := json.Marshal(imageConfig) 357 if err != nil { 358 return "", err 359 } 360 icDesc, err := blobsService.Put(ctx, schema2.MediaTypeImageConfig, icm) 361 if err != nil { 362 return "", err 363 } 364 icDesc.MediaType = schema2.MediaTypeImageConfig 365 man := schema2.Manifest{ 366 Versioned: schema2.SchemaVersion, 367 Config: icDesc, 368 Layers: []distribution.Descriptor{payloadDesc}, 369 } 370 dman, err := schema2.FromStruct(man) 371 if err != nil { 372 return "", err 373 } 374 dgst, err := manifestService.Put(ctx, dman, distribution.WithTag(pr.tag)) 375 return dgst.String(), err 376 }