Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 11 additions & 113 deletions internal/cmd/images/manifest_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,12 @@ package images
import (
"context"
"fmt"
"slices"
"strings"

goHelm "github.com/mittwald/go-helm-client"

"github.com/airbytehq/abctl/internal/common"
"github.com/airbytehq/abctl/internal/helm"
"github.com/airbytehq/abctl/internal/trace"
helmlib "github.com/mittwald/go-helm-client"
"helm.sh/helm/v3/pkg/repo"

appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme"
)

type ManifestCmd struct {
Expand All @@ -30,7 +21,13 @@ func (c *ManifestCmd) Run(ctx context.Context) error {
ctx, span := trace.NewSpan(ctx, "images manifest")
defer span.End()

images, err := c.findAirbyteImages(ctx)
// TODO (bernielomax): Replace with service manager client factory.
client, err := goHelm.New(helm.ClientOptions(common.AirbyteNamespace))
if err != nil {
return err
}

images, err := c.findAirbyteImages(ctx, client)
if err != nil {
return err
}
Expand All @@ -42,7 +39,7 @@ func (c *ManifestCmd) Run(ctx context.Context) error {
return nil
}

func (c *ManifestCmd) findAirbyteImages(ctx context.Context) ([]string, error) {
func (c *ManifestCmd) findAirbyteImages(ctx context.Context, helmClient goHelm.Client) ([]string, error) {
valuesYaml, err := helm.BuildAirbyteValues(ctx, helm.ValuesOpts{
ValuesFile: c.Values,
})
Expand All @@ -51,104 +48,5 @@ func (c *ManifestCmd) findAirbyteImages(ctx context.Context) ([]string, error) {
}

airbyteChartLoc := helm.LocateLatestAirbyteChart(c.ChartVersion, c.Chart)
return FindImagesFromChart(valuesYaml, airbyteChartLoc, c.ChartVersion)
}

func FindImagesFromChart(valuesYaml, chartName, chartVersion string) ([]string, error) {

// sharing a helm client with the install code causes some weird issues,
// and templating the chart doesn't need details about the k8s provider,
// we create a throwaway helm client here.
client, err := helmlib.New(helm.ClientOptions(common.AirbyteNamespace))
if err != nil {
return nil, err
}

err = client.AddOrUpdateChartRepo(repo.Entry{
Name: common.AirbyteRepoName,
URL: common.AirbyteRepoURL,
})
if err != nil {
return nil, err
}

bytes, err := client.TemplateChart(&helmlib.ChartSpec{
ChartName: chartName,
GenerateName: true,
ValuesYaml: valuesYaml,
Version: chartVersion,
}, nil)
if err != nil {
return nil, err
}

images := findAllImages(string(bytes))
return images, nil
}

// findAllImages walks through the Helm chart, looking for container images in k8s PodSpecs.
// It also looks for env vars in the airbyte-env config map that end with "_IMAGE".
// It returns a unique, sorted list of images found.
func findAllImages(chartYaml string) []string {
objs := decodeK8sResources(chartYaml)
imageSet := common.Set[string]{}

for _, obj := range objs {

var podSpec *corev1.PodSpec
switch z := obj.(type) {
case *corev1.ConfigMap:
if strings.HasSuffix(z.Name, "airbyte-env") {
for k, v := range z.Data {
if strings.HasSuffix(k, "_IMAGE") {
imageSet.Add(v)
}
}
}
continue
case *corev1.Pod:
podSpec = &z.Spec
case *batchv1.Job:
podSpec = &z.Spec.Template.Spec
case *appsv1.Deployment:
podSpec = &z.Spec.Template.Spec
case *appsv1.StatefulSet:
podSpec = &z.Spec.Template.Spec
default:
continue
}

for _, c := range podSpec.InitContainers {
imageSet.Add(c.Image)
}
for _, c := range podSpec.Containers {
imageSet.Add(c.Image)
}
}

var out []string
for _, k := range imageSet.Items() {
if k != "" {
out = append(out, k)
}
}
slices.Sort(out)

return out
}

func decodeK8sResources(renderedYaml string) []runtime.Object {
out := []runtime.Object{}
chunks := strings.Split(renderedYaml, "---")
for _, chunk := range chunks {
if len(chunk) == 0 {
continue
}
obj, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(chunk), nil, nil)
if err != nil {
continue
}
out = append(out, obj)
}
return out
return helm.FindImagesFromChart(helmClient, valuesYaml, airbyteChartLoc, c.ChartVersion)
}
114 changes: 0 additions & 114 deletions internal/cmd/images/manifest_cmd_test.go

This file was deleted.

108 changes: 108 additions & 0 deletions internal/helm/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package helm

import (
"context"
"slices"
"strings"

goHelm "github.com/mittwald/go-helm-client"
"helm.sh/helm/v3/pkg/repo"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/kubectl/pkg/scheme"

"github.com/airbytehq/abctl/internal/common"
)

func FindImagesFromChart(client goHelm.Client, valuesYaml, chartName, chartVersion string) ([]string, error) {
err := client.AddOrUpdateChartRepo(repo.Entry{
Name: common.AirbyteRepoName,
URL: common.AirbyteRepoURL,
})
if err != nil {
return nil, err
}

rel, err := client.InstallChart(context.TODO(), &goHelm.ChartSpec{
Copy link
Contributor Author

@bernielomax bernielomax Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change from the original function (apart from Helm client dependency injection) is replacing local template rendering (TemplateChart) with InstallChart in dry-run mode.

  • TemplateChart uses local rendering, which does not talk to Kubernetes and can clear or ignore the client’s discovered capabilities, requiring a throwaway Helm client to avoid issues.
  • InstallChart (with DryRun: true) connects to the cluster, preserves discovered capabilities, and uses them when rendering. Something not possible with local rendering.

ChartName: chartName,
GenerateName: true,
ValuesYaml: valuesYaml,
Version: chartVersion,
DryRun: true,
}, nil)
if err != nil {
return nil, err
}

images := findAllImages(rel.Manifest)
return images, nil
}

// findAllImages walks through the Helm chart, looking for container images in k8s PodSpecs.
// It also looks for env vars in the airbyte-env config map that end with "_IMAGE".
// It returns a unique, sorted list of images found.
func findAllImages(chartYaml string) []string {
objs := decodeK8sResources(chartYaml)
imageSet := common.Set[string]{}

for _, obj := range objs {

var podSpec *corev1.PodSpec
switch z := obj.(type) {
case *corev1.ConfigMap:
if strings.HasSuffix(z.Name, "airbyte-env") {
for k, v := range z.Data {
if strings.HasSuffix(k, "_IMAGE") {
imageSet.Add(v)
}
}
}
continue
case *corev1.Pod:
podSpec = &z.Spec
case *batchv1.Job:
podSpec = &z.Spec.Template.Spec
case *appsv1.Deployment:
podSpec = &z.Spec.Template.Spec
case *appsv1.StatefulSet:
podSpec = &z.Spec.Template.Spec
default:
continue
}

for _, c := range podSpec.InitContainers {
imageSet.Add(c.Image)
}
for _, c := range podSpec.Containers {
imageSet.Add(c.Image)
}
}

var out []string
for _, k := range imageSet.Items() {
if k != "" {
out = append(out, k)
}
}
slices.Sort(out)

return out
}

func decodeK8sResources(renderedYaml string) []runtime.Object {
out := []runtime.Object{}
chunks := strings.Split(renderedYaml, "---")
for _, chunk := range chunks {
if len(chunk) == 0 {
continue
}
obj, _, err := scheme.Codecs.UniversalDeserializer().Decode([]byte(chunk), nil, nil)
if err != nil {
continue
}
out = append(out, obj)
}
return out
}
Loading