Skip to content

Commit

Permalink
Adds experimental support for generating Carvel bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
mpermar authored Sep 25, 2023
2 parents fe9bfcb + 9f72b3e commit 71c35c8
Show file tree
Hide file tree
Showing 17 changed files with 585 additions and 16 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ images:
...
```

### Annotating a chart (EXPERIMENTAL)
### Annotating a Helm chart (EXPERIMENTAL)

`Images.lock` creation relies on the existence of the special images annotation inside `Chart.yaml`. If you have a Helm chart that does not contain any annotations, this command can be used to guess and generate an annotation with a tentative list of images. It's important to note that this list is a **best-effort** as the list of images is obtained from the `values.yaml` file and this is always an unreliable, often incomplete, and error-prone source as the configuration in `values.yaml` is very variable.

Expand All @@ -414,6 +414,21 @@ $ helm dt chart annotate examples/mariadb
INFO[0000] Helm chart annotated successfully
```

### Converting a Helm chart into a Carvel bundle (EXPERIMENTAL)

From `dt` 0.1.1 we have introduced a new command to create a [Carvel bundle](https://carvel.dev/imgpkg/docs/v0.37.x/resources/#bundle) from any Helm chart.


```console
$ helm dt chart carvelize examples/postgresql
✔ Helm chart "examples/postgresql" lock is valid
» Generating Carvel bundle for Helm chart "examples/postgresql"
✔ Validating Carvel images lock
✔ Carvel images lock written to "examples/postgresql/.imgpkg/images.yml"
✔ Carvel metadata written to "examples/postgresql/.imgpkg/bundle.yml"
🎉 Carvel bundle created successfully
```

## Frequently Asked Questions

### I cannot install the plugin due to `Error: Unable to update repository: exit status 1`
Expand Down
156 changes: 156 additions & 0 deletions carvel/carvel.go
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 ""
}
141 changes: 141 additions & 0 deletions cmd/dt/carvelize.go
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)
}
Loading

0 comments on commit 71c35c8

Please sign in to comment.