-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds experimental support for generating Carvel bundles
- Loading branch information
Showing
17 changed files
with
585 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// Package carvel implements experimental Carvel support | ||
package carvel | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/vmware-labs/distribution-tooling-for-helm/chartutils" | ||
"github.com/vmware-labs/distribution-tooling-for-helm/imagelock" | ||
"github.com/vmware-tanzu/carvel-imgpkg/pkg/imgpkg/lockconfig" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// CarvelBundleFilePath represents the usual bundle file for Carvel packaging | ||
const CarvelBundleFilePath = ".imgpkg/bundle.yml" | ||
|
||
// CarvelImagesFilePath represents the usual images file for Carvel packaging | ||
const CarvelImagesFilePath = ".imgpkg/images.yml" | ||
|
||
const carvelID = "kbld.carvel.dev/id" | ||
|
||
// Somehow there is no data structure for a bundle in Carvel. Copying some basics from the describe command. | ||
|
||
// Author information from a Bundle | ||
type Author struct { | ||
Name string `json:"name,omitempty"` | ||
Email string `json:"email,omitempty"` | ||
} | ||
|
||
// Website URL where more information of the Bundle can be found | ||
type Website struct { | ||
URL string `json:"url,omitempty"` | ||
} | ||
|
||
// Bundle Metadata | ||
const ( | ||
BundleAPIVersion = "imgpkg.carvel.dev/v1alpha1" | ||
BundleKind = "Bundle" | ||
) | ||
|
||
// BundleVersion with detailsa bout the Carvel bundle version | ||
type BundleVersion struct { | ||
APIVersion string `json:"apiVersion"` // This generated yaml, but due to lib we need to use `json` | ||
Kind string `json:"kind"` // This generated yaml, but due to lib we need to use `json` | ||
} | ||
|
||
// Metadata for a Carvel bundle | ||
type Metadata struct { | ||
Version BundleVersion | ||
Metadata map[string]string `json:"metadata,omitempty"` | ||
Authors []Author `json:"authors,omitempty"` | ||
Websites []Website `json:"websites,omitempty"` | ||
} | ||
|
||
// ToYAML serializes the Carvel bundle into YAML | ||
func (il *Metadata) ToYAML(w io.Writer) error { | ||
enc := yaml.NewEncoder(w) | ||
enc.SetIndent(2) | ||
|
||
return enc.Encode(il) | ||
} | ||
|
||
// NewCarvelBundle returns a new carvel bundle Metadata instance | ||
func NewCarvelBundle() *Metadata { | ||
return &Metadata{ | ||
Version: BundleVersion{ | ||
APIVersion: BundleAPIVersion, | ||
Kind: BundleKind, | ||
}, | ||
Metadata: map[string]string{}, | ||
Authors: []Author{}, | ||
Websites: []Website{}, | ||
} | ||
} | ||
|
||
// CreateBundleMetadata builds and sets a new Carvel bundle struct | ||
func CreateBundleMetadata(chartPath string, lock *imagelock.ImagesLock, cfg *chartutils.Configuration) (*Metadata, error) { | ||
bundleMetadata := NewCarvelBundle() | ||
|
||
chart, err := chartutils.LoadChart(chartPath) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to load chart: %w", err) | ||
} | ||
|
||
for _, maintainer := range chart.Metadata.Maintainers { | ||
author := Author{ | ||
Name: maintainer.Name, | ||
} | ||
author.Email = maintainer.Email | ||
bundleMetadata.Authors = append(bundleMetadata.Authors, author) | ||
} | ||
for _, source := range chart.Metadata.Sources { | ||
website := Website{ | ||
URL: source, | ||
} | ||
bundleMetadata.Websites = append(bundleMetadata.Websites, website) | ||
} | ||
|
||
bundleMetadata.Metadata["name"] = lock.Chart.Name | ||
for key, value := range chart.Metadata.Annotations { | ||
annotationsKey := cfg.AnnotationsKey | ||
if annotationsKey == "" { | ||
annotationsKey = imagelock.DefaultAnnotationsKey | ||
} | ||
if key != annotationsKey { | ||
bundleMetadata.Metadata[key] = value | ||
} | ||
} | ||
return bundleMetadata, nil | ||
} | ||
|
||
// CreateImagesLock builds and set a new Carvel images lock struct | ||
func CreateImagesLock(lock *imagelock.ImagesLock) (lockconfig.ImagesLock, error) { | ||
imagesLock := lockconfig.ImagesLock{ | ||
LockVersion: lockconfig.LockVersion{ | ||
APIVersion: lockconfig.ImagesLockAPIVersion, | ||
Kind: lockconfig.ImagesLockKind, | ||
}, | ||
} | ||
for _, img := range lock.Images { | ||
// Carvel does not seem to support multi-arch. Grab amd64 digest | ||
|
||
name := img.Image | ||
i := strings.LastIndex(img.Image, ":") | ||
if i > -1 { | ||
name = img.Image[0:i] | ||
} | ||
//TODO: Clarify with Carvel community their multi-arch support | ||
//for the time being we stick to amd64 | ||
imageWithDigest := getIntelImageWithDigest(name, img) | ||
if imageWithDigest == "" { | ||
// See above. Skip | ||
break | ||
} | ||
imageRef := lockconfig.ImageRef{ | ||
Image: imageWithDigest, | ||
Annotations: map[string]string{ | ||
carvelID: img.Image, | ||
}, | ||
} | ||
imagesLock.AddImageRef(imageRef) | ||
} | ||
return imagesLock, nil | ||
} | ||
|
||
func getIntelImageWithDigest(name string, img *imagelock.ChartImage) string { | ||
|
||
for _, digest := range img.Digests { | ||
if digest.Arch == "linux/amd64" { | ||
return fmt.Sprintf("%s@%s", name, digest.Digest.String()) | ||
} | ||
} | ||
return "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/vmware-labs/distribution-tooling-for-helm/carvel" | ||
"github.com/vmware-labs/distribution-tooling-for-helm/chartutils" | ||
"github.com/vmware-labs/distribution-tooling-for-helm/internal/log" | ||
"github.com/vmware-labs/distribution-tooling-for-helm/utils" | ||
) | ||
|
||
var carvelizeCmd = newCarvelizeCmd() | ||
|
||
func newCarvelizeCmd() *cobra.Command { | ||
var yamlFormat bool | ||
var showDetails bool | ||
|
||
cmd := &cobra.Command{ | ||
Use: "carvelize FILE", | ||
Short: "Adds a Carvel bundle to the Helm chart (Experimental)", | ||
Long: `Experimental. Adds a Carvel bundle to an existing Helm chart`, | ||
Example: ` # Adds a Carvel bundle to a Helm chart | ||
$ dt charts carvelize examples/mariadb`, | ||
SilenceUsage: true, | ||
SilenceErrors: true, | ||
Args: cobra.ExactArgs(1), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
chartPath := args[0] | ||
l := getLogger() | ||
// Allows silencing called methods | ||
silentLog := log.SilentLog | ||
|
||
lockFile, err := getImageLockFilePath(chartPath) | ||
if err != nil { | ||
return fmt.Errorf("failed to determine Images.lock file location: %w", err) | ||
} | ||
|
||
if utils.FileExists(lockFile) { | ||
if err := l.ExecuteStep("Verifying Images.lock", func() error { | ||
return verifyLock(chartPath, lockFile) | ||
}); err != nil { | ||
return l.Failf("Failed to verify lock: %w", err) | ||
} | ||
l.Infof("Helm chart %q lock is valid", chartPath) | ||
|
||
} else { | ||
err := l.ExecuteStep( | ||
"Images.lock file does not exist. Generating it from annotations...", | ||
func() error { | ||
return createImagesLock(chartPath, | ||
lockFile, silentLog, | ||
) | ||
}, | ||
) | ||
if err != nil { | ||
return l.Failf("Failed to generate lock: %w", err) | ||
} | ||
l.Infof("Images.lock file written to %q", lockFile) | ||
} | ||
if err := l.Section(fmt.Sprintf("Generating Carvel bundle for Helm chart %q", chartPath), func(childLog log.SectionLogger) error { | ||
if err := generateCarvelBundle( | ||
chartPath, | ||
chartutils.WithAnnotationsKey(getAnnotationsKey()), | ||
chartutils.WithLog(childLog), | ||
); err != nil { | ||
return childLog.Failf("%v", err) | ||
} | ||
return nil | ||
}); err != nil { | ||
return l.Failf("%w", err) | ||
} | ||
l.Successf("Carvel bundle created successfully") | ||
return nil | ||
}, | ||
} | ||
cmd.PersistentFlags().BoolVar(&yamlFormat, "yaml", yamlFormat, "Show report in YAML format") | ||
cmd.PersistentFlags().BoolVar(&showDetails, "detailed", showDetails, "When using the printable report, add more details about the bundled images") | ||
|
||
return cmd | ||
} | ||
|
||
func generateCarvelBundle(chartPath string, opts ...chartutils.Option) error { | ||
cfg := chartutils.NewConfiguration(opts...) | ||
l := cfg.Log | ||
|
||
lock, err := readLockFromWrap(chartPath) | ||
if err != nil { | ||
return fmt.Errorf("failed to load Images.lock: %v", err) | ||
} | ||
|
||
imgPkgPath := filepath.Join(chartPath, ".imgpkg") | ||
if !utils.FileExists(imgPkgPath) { | ||
err := os.Mkdir(imgPkgPath, os.FileMode(0755)) | ||
if err != nil { | ||
return fmt.Errorf("failed to create .imgpkg directory: %w", err) | ||
} | ||
} | ||
|
||
bundleMetadata, err := carvel.CreateBundleMetadata(chartPath, lock, cfg) | ||
if err != nil { | ||
return fmt.Errorf("failed to prepare Carvel bundle: %w", err) | ||
} | ||
|
||
carvelImagesLock, err := carvel.CreateImagesLock(lock) | ||
if err != nil { | ||
return fmt.Errorf("failed to prepare Carvel images lock: %w", err) | ||
} | ||
l.Infof("Validating Carvel images lock") | ||
|
||
err = carvelImagesLock.Validate() | ||
if err != nil { | ||
return fmt.Errorf("failed to validate Carvel images lock: %w", err) | ||
} | ||
|
||
path := filepath.Join(imgPkgPath, "images.yml") | ||
err = carvelImagesLock.WriteToPath(path) | ||
if err != nil { | ||
return fmt.Errorf("Could not write image lock: %v", err) | ||
} | ||
l.Infof("Carvel images lock written to %q", path) | ||
|
||
buff := &bytes.Buffer{} | ||
if err = bundleMetadata.ToYAML(buff); err != nil { | ||
return fmt.Errorf("failed to write bundle metadata file: %v", err) | ||
} | ||
|
||
path = imgPkgPath + "/bundle.yml" | ||
if err := os.WriteFile(path, buff.Bytes(), 0666); err != nil { | ||
return fmt.Errorf("failed to write Carvel bundle metadata to %q: %w", path, err) | ||
} | ||
l.Infof("Carvel metadata written to %q", path) | ||
return nil | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(infoCmd) | ||
} |
Oops, something went wrong.