github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/registry/client.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package registry // import "github.com/stefanmcshane/helm/pkg/registry" 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/http" 26 "sort" 27 "strings" 28 29 "github.com/Masterminds/semver/v3" 30 "github.com/containerd/containerd/remotes" 31 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 32 "github.com/pkg/errors" 33 "oras.land/oras-go/pkg/auth" 34 dockerauth "oras.land/oras-go/pkg/auth/docker" 35 "oras.land/oras-go/pkg/content" 36 "oras.land/oras-go/pkg/oras" 37 "oras.land/oras-go/pkg/registry" 38 registryremote "oras.land/oras-go/pkg/registry/remote" 39 registryauth "oras.land/oras-go/pkg/registry/remote/auth" 40 41 "github.com/stefanmcshane/helm/internal/version" 42 "github.com/stefanmcshane/helm/pkg/chart" 43 "github.com/stefanmcshane/helm/pkg/helmpath" 44 ) 45 46 // See https://github.com/helm/helm/issues/10166 47 const registryUnderscoreMessage = ` 48 OCI artifact references (e.g. tags) do not support the plus sign (+). To support 49 storing semantic versions, Helm adopts the convention of changing plus (+) to 50 an underscore (_) in chart version tags when pushing to a registry and back to 51 a plus (+) when pulling from a registry.` 52 53 type ( 54 // Client works with OCI-compliant registries 55 Client struct { 56 debug bool 57 enableCache bool 58 // path to repository config file e.g. ~/.docker/config.json 59 credentialsFile string 60 out io.Writer 61 authorizer auth.Client 62 registryAuthorizer *registryauth.Client 63 resolver remotes.Resolver 64 } 65 66 // ClientOption allows specifying various settings configurable by the user for overriding the defaults 67 // used when creating a new default client 68 ClientOption func(*Client) 69 ) 70 71 // NewClient returns a new registry client with config 72 func NewClient(options ...ClientOption) (*Client, error) { 73 client := &Client{ 74 out: ioutil.Discard, 75 } 76 for _, option := range options { 77 option(client) 78 } 79 if client.credentialsFile == "" { 80 client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) 81 } 82 if client.authorizer == nil { 83 authClient, err := dockerauth.NewClientWithDockerFallback(client.credentialsFile) 84 if err != nil { 85 return nil, err 86 } 87 client.authorizer = authClient 88 } 89 if client.resolver == nil { 90 headers := http.Header{} 91 headers.Set("User-Agent", version.GetUserAgent()) 92 opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} 93 resolver, err := client.authorizer.ResolverWithOpts(opts...) 94 if err != nil { 95 return nil, err 96 } 97 client.resolver = resolver 98 } 99 100 // allocate a cache if option is set 101 var cache registryauth.Cache 102 if client.enableCache { 103 cache = registryauth.DefaultCache 104 } 105 if client.registryAuthorizer == nil { 106 client.registryAuthorizer = ®istryauth.Client{ 107 Header: http.Header{ 108 "User-Agent": {version.GetUserAgent()}, 109 }, 110 Cache: cache, 111 Credential: func(ctx context.Context, reg string) (registryauth.Credential, error) { 112 dockerClient, ok := client.authorizer.(*dockerauth.Client) 113 if !ok { 114 return registryauth.EmptyCredential, errors.New("unable to obtain docker client") 115 } 116 117 username, password, err := dockerClient.Credential(reg) 118 if err != nil { 119 return registryauth.EmptyCredential, errors.New("unable to retrieve credentials") 120 } 121 122 // A blank returned username and password value is a bearer token 123 if username == "" && password != "" { 124 return registryauth.Credential{ 125 RefreshToken: password, 126 }, nil 127 } 128 129 return registryauth.Credential{ 130 Username: username, 131 Password: password, 132 }, nil 133 134 }, 135 } 136 137 } 138 return client, nil 139 } 140 141 // ClientOptDebug returns a function that sets the debug setting on client options set 142 func ClientOptDebug(debug bool) ClientOption { 143 return func(client *Client) { 144 client.debug = debug 145 } 146 } 147 148 // ClientOptEnableCache returns a function that sets the enableCache setting on a client options set 149 func ClientOptEnableCache(enableCache bool) ClientOption { 150 return func(client *Client) { 151 client.enableCache = enableCache 152 } 153 } 154 155 // ClientOptWriter returns a function that sets the writer setting on client options set 156 func ClientOptWriter(out io.Writer) ClientOption { 157 return func(client *Client) { 158 client.out = out 159 } 160 } 161 162 // ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set 163 func ClientOptCredentialsFile(credentialsFile string) ClientOption { 164 return func(client *Client) { 165 client.credentialsFile = credentialsFile 166 } 167 } 168 169 type ( 170 // LoginOption allows specifying various settings on login 171 LoginOption func(*loginOperation) 172 173 loginOperation struct { 174 username string 175 password string 176 insecure bool 177 } 178 ) 179 180 // Login logs into a registry 181 func (c *Client) Login(host string, options ...LoginOption) error { 182 operation := &loginOperation{} 183 for _, option := range options { 184 option(operation) 185 } 186 authorizerLoginOpts := []auth.LoginOption{ 187 auth.WithLoginContext(ctx(c.out, c.debug)), 188 auth.WithLoginHostname(host), 189 auth.WithLoginUsername(operation.username), 190 auth.WithLoginSecret(operation.password), 191 auth.WithLoginUserAgent(version.GetUserAgent()), 192 } 193 if operation.insecure { 194 authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure()) 195 } 196 if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil { 197 return err 198 } 199 fmt.Fprintln(c.out, "Login Succeeded") 200 return nil 201 } 202 203 // LoginOptBasicAuth returns a function that sets the username/password settings on login 204 func LoginOptBasicAuth(username string, password string) LoginOption { 205 return func(operation *loginOperation) { 206 operation.username = username 207 operation.password = password 208 } 209 } 210 211 // LoginOptInsecure returns a function that sets the insecure setting on login 212 func LoginOptInsecure(insecure bool) LoginOption { 213 return func(operation *loginOperation) { 214 operation.insecure = insecure 215 } 216 } 217 218 type ( 219 // LogoutOption allows specifying various settings on logout 220 LogoutOption func(*logoutOperation) 221 222 logoutOperation struct{} 223 ) 224 225 // Logout logs out of a registry 226 func (c *Client) Logout(host string, opts ...LogoutOption) error { 227 operation := &logoutOperation{} 228 for _, opt := range opts { 229 opt(operation) 230 } 231 if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil { 232 return err 233 } 234 fmt.Fprintf(c.out, "Removing login credentials for %s\n", host) 235 return nil 236 } 237 238 type ( 239 // PullOption allows specifying various settings on pull 240 PullOption func(*pullOperation) 241 242 // PullResult is the result returned upon successful pull. 243 PullResult struct { 244 Manifest *descriptorPullSummary `json:"manifest"` 245 Config *descriptorPullSummary `json:"config"` 246 Chart *descriptorPullSummaryWithMeta `json:"chart"` 247 Prov *descriptorPullSummary `json:"prov"` 248 Ref string `json:"ref"` 249 } 250 251 descriptorPullSummary struct { 252 Data []byte `json:"-"` 253 Digest string `json:"digest"` 254 Size int64 `json:"size"` 255 } 256 257 descriptorPullSummaryWithMeta struct { 258 descriptorPullSummary 259 Meta *chart.Metadata `json:"meta"` 260 } 261 262 pullOperation struct { 263 withChart bool 264 withProv bool 265 ignoreMissingProv bool 266 } 267 ) 268 269 // Pull downloads a chart from a registry 270 func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { 271 parsedRef, err := parseReference(ref) 272 if err != nil { 273 return nil, err 274 } 275 276 operation := &pullOperation{ 277 withChart: true, // By default, always download the chart layer 278 } 279 for _, option := range options { 280 option(operation) 281 } 282 if !operation.withChart && !operation.withProv { 283 return nil, errors.New( 284 "must specify at least one layer to pull (chart/prov)") 285 } 286 memoryStore := content.NewMemory() 287 allowedMediaTypes := []string{ 288 ConfigMediaType, 289 } 290 minNumDescriptors := 1 // 1 for the config 291 if operation.withChart { 292 minNumDescriptors++ 293 allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType) 294 } 295 if operation.withProv { 296 if !operation.ignoreMissingProv { 297 minNumDescriptors++ 298 } 299 allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType) 300 } 301 302 var descriptors, layers []ocispec.Descriptor 303 registryStore := content.Registry{Resolver: c.resolver} 304 305 manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "", 306 oras.WithPullEmptyNameAllowed(), 307 oras.WithAllowedMediaTypes(allowedMediaTypes), 308 oras.WithLayerDescriptors(func(l []ocispec.Descriptor) { 309 layers = l 310 })) 311 if err != nil { 312 return nil, err 313 } 314 315 descriptors = append(descriptors, manifest) 316 descriptors = append(descriptors, layers...) 317 318 numDescriptors := len(descriptors) 319 if numDescriptors < minNumDescriptors { 320 return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", 321 minNumDescriptors, numDescriptors) 322 } 323 var configDescriptor *ocispec.Descriptor 324 var chartDescriptor *ocispec.Descriptor 325 var provDescriptor *ocispec.Descriptor 326 for _, descriptor := range descriptors { 327 d := descriptor 328 switch d.MediaType { 329 case ConfigMediaType: 330 configDescriptor = &d 331 case ChartLayerMediaType: 332 chartDescriptor = &d 333 case ProvLayerMediaType: 334 provDescriptor = &d 335 case LegacyChartLayerMediaType: 336 chartDescriptor = &d 337 fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType) 338 } 339 } 340 if configDescriptor == nil { 341 return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType) 342 } 343 if operation.withChart && chartDescriptor == nil { 344 return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", 345 ChartLayerMediaType) 346 } 347 var provMissing bool 348 if operation.withProv && provDescriptor == nil { 349 if operation.ignoreMissingProv { 350 provMissing = true 351 } else { 352 return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", 353 ProvLayerMediaType) 354 } 355 } 356 result := &PullResult{ 357 Manifest: &descriptorPullSummary{ 358 Digest: manifest.Digest.String(), 359 Size: manifest.Size, 360 }, 361 Config: &descriptorPullSummary{ 362 Digest: configDescriptor.Digest.String(), 363 Size: configDescriptor.Size, 364 }, 365 Chart: &descriptorPullSummaryWithMeta{}, 366 Prov: &descriptorPullSummary{}, 367 Ref: parsedRef.String(), 368 } 369 var getManifestErr error 370 if _, manifestData, ok := memoryStore.Get(manifest); !ok { 371 getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest) 372 } else { 373 result.Manifest.Data = manifestData 374 } 375 if getManifestErr != nil { 376 return nil, getManifestErr 377 } 378 var getConfigDescriptorErr error 379 if _, configData, ok := memoryStore.Get(*configDescriptor); !ok { 380 getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest) 381 } else { 382 result.Config.Data = configData 383 var meta *chart.Metadata 384 if err := json.Unmarshal(configData, &meta); err != nil { 385 return nil, err 386 } 387 result.Chart.Meta = meta 388 } 389 if getConfigDescriptorErr != nil { 390 return nil, getConfigDescriptorErr 391 } 392 if operation.withChart { 393 var getChartDescriptorErr error 394 if _, chartData, ok := memoryStore.Get(*chartDescriptor); !ok { 395 getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest) 396 } else { 397 result.Chart.Data = chartData 398 result.Chart.Digest = chartDescriptor.Digest.String() 399 result.Chart.Size = chartDescriptor.Size 400 } 401 if getChartDescriptorErr != nil { 402 return nil, getChartDescriptorErr 403 } 404 } 405 if operation.withProv && !provMissing { 406 var getProvDescriptorErr error 407 if _, provData, ok := memoryStore.Get(*provDescriptor); !ok { 408 getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest) 409 } else { 410 result.Prov.Data = provData 411 result.Prov.Digest = provDescriptor.Digest.String() 412 result.Prov.Size = provDescriptor.Size 413 } 414 if getProvDescriptorErr != nil { 415 return nil, getProvDescriptorErr 416 } 417 } 418 419 fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref) 420 fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) 421 422 if strings.Contains(result.Ref, "_") { 423 fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) 424 fmt.Fprint(c.out, registryUnderscoreMessage+"\n") 425 } 426 427 return result, nil 428 } 429 430 // PullOptWithChart returns a function that sets the withChart setting on pull 431 func PullOptWithChart(withChart bool) PullOption { 432 return func(operation *pullOperation) { 433 operation.withChart = withChart 434 } 435 } 436 437 // PullOptWithProv returns a function that sets the withProv setting on pull 438 func PullOptWithProv(withProv bool) PullOption { 439 return func(operation *pullOperation) { 440 operation.withProv = withProv 441 } 442 } 443 444 // PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull 445 func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption { 446 return func(operation *pullOperation) { 447 operation.ignoreMissingProv = ignoreMissingProv 448 } 449 } 450 451 type ( 452 // PushOption allows specifying various settings on push 453 PushOption func(*pushOperation) 454 455 // PushResult is the result returned upon successful push. 456 PushResult struct { 457 Manifest *descriptorPushSummary `json:"manifest"` 458 Config *descriptorPushSummary `json:"config"` 459 Chart *descriptorPushSummaryWithMeta `json:"chart"` 460 Prov *descriptorPushSummary `json:"prov"` 461 Ref string `json:"ref"` 462 } 463 464 descriptorPushSummary struct { 465 Digest string `json:"digest"` 466 Size int64 `json:"size"` 467 } 468 469 descriptorPushSummaryWithMeta struct { 470 descriptorPushSummary 471 Meta *chart.Metadata `json:"meta"` 472 } 473 474 pushOperation struct { 475 provData []byte 476 strictMode bool 477 } 478 ) 479 480 // Push uploads a chart to a registry. 481 func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) { 482 parsedRef, err := parseReference(ref) 483 if err != nil { 484 return nil, err 485 } 486 487 operation := &pushOperation{ 488 strictMode: true, // By default, enable strict mode 489 } 490 for _, option := range options { 491 option(operation) 492 } 493 meta, err := extractChartMeta(data) 494 if err != nil { 495 return nil, err 496 } 497 if operation.strictMode { 498 if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) { 499 return nil, errors.New( 500 "strict mode enabled, ref basename and tag must match the chart name and version") 501 } 502 } 503 memoryStore := content.NewMemory() 504 chartDescriptor, err := memoryStore.Add("", ChartLayerMediaType, data) 505 if err != nil { 506 return nil, err 507 } 508 509 configData, err := json.Marshal(meta) 510 if err != nil { 511 return nil, err 512 } 513 514 configDescriptor, err := memoryStore.Add("", ConfigMediaType, configData) 515 if err != nil { 516 return nil, err 517 } 518 519 descriptors := []ocispec.Descriptor{chartDescriptor} 520 var provDescriptor ocispec.Descriptor 521 if operation.provData != nil { 522 provDescriptor, err = memoryStore.Add("", ProvLayerMediaType, operation.provData) 523 if err != nil { 524 return nil, err 525 } 526 527 descriptors = append(descriptors, provDescriptor) 528 } 529 530 manifestData, manifest, err := content.GenerateManifest(&configDescriptor, nil, descriptors...) 531 if err != nil { 532 return nil, err 533 } 534 535 if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { 536 return nil, err 537 } 538 539 registryStore := content.Registry{Resolver: c.resolver} 540 _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "", 541 oras.WithNameValidation(nil)) 542 if err != nil { 543 return nil, err 544 } 545 chartSummary := &descriptorPushSummaryWithMeta{ 546 Meta: meta, 547 } 548 chartSummary.Digest = chartDescriptor.Digest.String() 549 chartSummary.Size = chartDescriptor.Size 550 result := &PushResult{ 551 Manifest: &descriptorPushSummary{ 552 Digest: manifest.Digest.String(), 553 Size: manifest.Size, 554 }, 555 Config: &descriptorPushSummary{ 556 Digest: configDescriptor.Digest.String(), 557 Size: configDescriptor.Size, 558 }, 559 Chart: chartSummary, 560 Prov: &descriptorPushSummary{}, // prevent nil references 561 Ref: parsedRef.String(), 562 } 563 if operation.provData != nil { 564 result.Prov = &descriptorPushSummary{ 565 Digest: provDescriptor.Digest.String(), 566 Size: provDescriptor.Size, 567 } 568 } 569 fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) 570 fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) 571 if strings.Contains(parsedRef.Reference, "_") { 572 fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) 573 fmt.Fprint(c.out, registryUnderscoreMessage+"\n") 574 } 575 576 return result, err 577 } 578 579 // PushOptProvData returns a function that sets the prov bytes setting on push 580 func PushOptProvData(provData []byte) PushOption { 581 return func(operation *pushOperation) { 582 operation.provData = provData 583 } 584 } 585 586 // PushOptStrictMode returns a function that sets the strictMode setting on push 587 func PushOptStrictMode(strictMode bool) PushOption { 588 return func(operation *pushOperation) { 589 operation.strictMode = strictMode 590 } 591 } 592 593 // Tags provides a sorted list all semver compliant tags for a given repository 594 func (c *Client) Tags(ref string) ([]string, error) { 595 parsedReference, err := registry.ParseReference(ref) 596 if err != nil { 597 return nil, err 598 } 599 600 repository := registryremote.Repository{ 601 Reference: parsedReference, 602 Client: c.registryAuthorizer, 603 } 604 605 var registryTags []string 606 607 for { 608 registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository) 609 if err != nil { 610 // Fallback to http based request 611 if !repository.PlainHTTP && strings.Contains(err.Error(), "server gave HTTP response") { 612 repository.PlainHTTP = true 613 continue 614 } 615 return nil, err 616 } 617 618 break 619 620 } 621 622 var tagVersions []*semver.Version 623 for _, tag := range registryTags { 624 // Change underscore (_) back to plus (+) for Helm 625 // See https://github.com/helm/helm/issues/10166 626 tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+")) 627 if err == nil { 628 tagVersions = append(tagVersions, tagVersion) 629 } 630 } 631 632 // Sort the collection 633 sort.Sort(sort.Reverse(semver.Collection(tagVersions))) 634 635 tags := make([]string, len(tagVersions)) 636 637 for iTv, tv := range tagVersions { 638 tags[iTv] = tv.String() 639 } 640 641 return tags, nil 642 643 }