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.
helm/pkg/registry/plugin.go

213 lines
7.0 KiB

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"encoding/json"
"fmt"
"strings"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// Plugin-specific constants
const (
// PluginArtifactType is the artifact type for Helm plugins
PluginArtifactType = "application/vnd.helm.plugin.v1+json"
)
// PluginPullOptions configures a plugin pull operation
type PluginPullOptions struct {
// PluginName specifies the expected plugin name for layer validation
PluginName string
}
// PluginPullResult contains the result of a plugin pull operation
type PluginPullResult struct {
Manifest ocispec.Descriptor
PluginData []byte
Prov struct {
Data []byte
}
Ref string
PluginName string
}
// PullPlugin downloads a plugin from an OCI registry using artifact type
func (c *Client) PullPlugin(ref string, pluginName string, options ...PluginPullOption) (*PluginPullResult, error) {
operation := &pluginPullOperation{
pluginName: pluginName,
}
for _, option := range options {
option(operation)
}
// Use generic client for the pull operation with artifact type filtering
genericClient := c.Generic()
genericResult, err := genericClient.PullGeneric(ref, GenericPullOptions{
// Allow manifests and all layer types - we'll validate artifact type after download
AllowedMediaTypes: []string{
ocispec.MediaTypeImageManifest,
"application/vnd.oci.image.layer.v1.tar",
"application/vnd.oci.image.layer.v1.tar+gzip",
},
})
if err != nil {
return nil, err
}
// Process the result with plugin-specific logic
return c.processPluginPull(genericResult, operation.pluginName)
}
// processPluginPull handles plugin-specific processing of a generic pull result using artifact type
func (c *Client) processPluginPull(genericResult *GenericPullResult, pluginName string) (*PluginPullResult, error) {
// First validate that this is actually a plugin artifact
manifestData, err := c.Generic().GetDescriptorData(genericResult.MemoryStore, genericResult.Manifest)
if err != nil {
return nil, fmt.Errorf("unable to retrieve manifest: %w", err)
}
// Parse the manifest to check artifact type
var manifest ocispec.Manifest
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return nil, fmt.Errorf("unable to parse manifest: %w", err)
}
// Validate artifact type (for OCI v1.1+ manifests)
if manifest.ArtifactType != "" && manifest.ArtifactType != PluginArtifactType {
return nil, fmt.Errorf("expected artifact type %s, got %s", PluginArtifactType, manifest.ArtifactType)
}
// For backwards compatibility, also check config media type if no artifact type
if manifest.ArtifactType == "" && manifest.Config.MediaType != PluginArtifactType {
return nil, fmt.Errorf("expected config media type %s for legacy compatibility, got %s", PluginArtifactType, manifest.Config.MediaType)
}
// Find the plugin tarball and optional provenance using NAME-VERSION.tgz format
var pluginDescriptor *ocispec.Descriptor
var provenanceDescriptor *ocispec.Descriptor
var foundProvenanceName string
// Look for layers with the expected titles/annotations
for _, layer := range manifest.Layers {
d := layer
// Check for title annotation
if title, exists := d.Annotations[ocispec.AnnotationTitle]; exists {
// Check if this looks like a plugin tarball: {pluginName}-{version}.tgz
if pluginDescriptor == nil && strings.HasPrefix(title, pluginName+"-") && strings.HasSuffix(title, ".tgz") {
pluginDescriptor = &d
}
// Check if this looks like a plugin provenance: {pluginName}-{version}.tgz.prov
if provenanceDescriptor == nil && strings.HasPrefix(title, pluginName+"-") && strings.HasSuffix(title, ".tgz.prov") {
provenanceDescriptor = &d
foundProvenanceName = title
}
}
}
// Plugin tarball is required
if pluginDescriptor == nil {
return nil, fmt.Errorf("required layer matching pattern %s-VERSION.tgz not found in manifest", pluginName)
}
// Build plugin-specific result
result := &PluginPullResult{
Manifest: genericResult.Manifest,
Ref: genericResult.Ref,
PluginName: pluginName,
}
// Fetch plugin data using generic client
genericClient := c.Generic()
result.PluginData, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *pluginDescriptor)
if err != nil {
return nil, fmt.Errorf("unable to retrieve plugin data with digest %s: %w", pluginDescriptor.Digest, err)
}
// Fetch provenance data if available
if provenanceDescriptor != nil {
result.Prov.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *provenanceDescriptor)
if err != nil {
return nil, fmt.Errorf("unable to retrieve provenance data with digest %s: %w", provenanceDescriptor.Digest, err)
}
}
fmt.Fprintf(c.out, "Pulled plugin: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
if result.Prov.Data != nil {
fmt.Fprintf(c.out, "Provenance: %s\n", foundProvenanceName)
}
if strings.Contains(result.Ref, "_") {
fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref)
fmt.Fprint(c.out, registryUnderscoreMessage+"\n")
}
return result, nil
}
// Plugin pull operation types and options
type (
pluginPullOperation struct {
pluginName string
withProv bool
}
// PluginPullOption allows customizing plugin pull operations
PluginPullOption func(*pluginPullOperation)
)
// PluginPullOptWithPluginName sets the plugin name for validation
func PluginPullOptWithPluginName(name string) PluginPullOption {
return func(operation *pluginPullOperation) {
operation.pluginName = name
}
}
// GetPluginName extracts the plugin name from an OCI reference using proper reference parsing
func GetPluginName(source string) (string, error) {
ref, err := newReference(source)
if err != nil {
return "", fmt.Errorf("invalid OCI reference: %w", err)
}
// Extract plugin name from the repository path
// e.g., "ghcr.io/user/plugin-name:v1.0.0" -> Repository: "user/plugin-name"
repository := ref.Repository
if repository == "" {
return "", fmt.Errorf("invalid OCI reference: missing repository")
}
// Get the last part of the repository path as the plugin name
parts := strings.Split(repository, "/")
pluginName := parts[len(parts)-1]
if pluginName == "" {
return "", fmt.Errorf("invalid OCI reference: cannot determine plugin name from repository %s", repository)
}
return pluginName, nil
}
// PullPluginOptWithProv configures the pull to fetch provenance data
func PullPluginOptWithProv(withProv bool) PluginPullOption {
return func(operation *pluginPullOperation) {
operation.withProv = withProv
}
}