diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 605de5d0..87681bc2 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -191,7 +191,14 @@ jobs: /tmp/flux create kustomization flux-system \ --source=flux-system \ --path=./clusters/staging + kubectl -n flux-system wait kustomization/infrastructure --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m + kubectl -n nginx wait helmrelease/nginx --for=condition=ready --timeout=5m + kubectl -n redis wait helmrelease/redis --for=condition=ready --timeout=5m + kubectl -n podinfo wait helmrelease/podinfo --for=condition=ready --timeout=5m + - name: flux tree + run: | + /tmp/flux tree kustomization flux-system | grep Service/podinfo - name: flux check run: | /tmp/flux check diff --git a/cmd/flux/testdata/tree/kustomizations.yaml b/cmd/flux/testdata/tree/kustomizations.yaml index a5739d6f..45055da2 100644 --- a/cmd/flux/testdata/tree/kustomizations.yaml +++ b/cmd/flux/testdata/tree/kustomizations.yaml @@ -65,8 +65,6 @@ status: v: v1 - id: cert-manager_cert-manager_source.toolkit.fluxcd.io_HelmRepository v: v1beta1 - - id: cert-manager_cert-manager_helm.toolkit.fluxcd.io_HelmRelease - v: v2beta1 --- apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization @@ -87,3 +85,4 @@ status: reason: ReconciliationSucceeded status: "True" type: Ready +--- diff --git a/cmd/flux/testdata/tree/tree-compact.golden b/cmd/flux/testdata/tree/tree-compact.golden index a635f6c8..d4e8a4f9 100644 --- a/cmd/flux/testdata/tree/tree-compact.golden +++ b/cmd/flux/testdata/tree/tree-compact.golden @@ -1,6 +1,5 @@ Kustomization/{{ .fluxns }}/flux-system ├── Kustomization/{{ .fluxns }}/infrastructure -│ ├── HelmRepository/cert-manager/cert-manager -│ └── HelmRelease/cert-manager/cert-manager +│ └── HelmRepository/cert-manager/cert-manager └── GitRepository/{{ .fluxns }}/flux-system diff --git a/cmd/flux/testdata/tree/tree.golden b/cmd/flux/testdata/tree/tree.golden index e2af2008..c5863ed1 100644 --- a/cmd/flux/testdata/tree/tree.golden +++ b/cmd/flux/testdata/tree/tree.golden @@ -6,7 +6,6 @@ Kustomization/{{ .fluxns }}/flux-system ├── Deployment/{{ .fluxns }}/source-controller ├── Kustomization/{{ .fluxns }}/infrastructure │ ├── Namespace/cert-manager -│ ├── HelmRepository/cert-manager/cert-manager -│ └── HelmRelease/cert-manager/cert-manager +│ └── HelmRepository/cert-manager/cert-manager └── GitRepository/{{ .fluxns }}/flux-system diff --git a/cmd/flux/tree_kustomization.go b/cmd/flux/tree_kustomization.go index 79964d76..d3d43ec6 100644 --- a/cmd/flux/tree_kustomization.go +++ b/cmd/flux/tree_kustomization.go @@ -17,15 +17,23 @@ limitations under the License. package main import ( + "bytes" + "compress/gzip" "context" + "encoding/base64" "encoding/json" "fmt" + "io/ioutil" "strings" "github.com/fluxcd/flux2/internal/tree" "github.com/fluxcd/flux2/internal/utils" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + "github.com/fluxcd/pkg/ssa" "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" @@ -119,13 +127,15 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus return nil } + compactGroup := "toolkit.fluxcd.io" + for _, entry := range item.Status.Inventory.Entries { objMetadata, err := object.ParseObjMetadata(entry.ID) if err != nil { return err } - if compact && !strings.Contains(objMetadata.GroupKind.Group, "toolkit.fluxcd.io") { + if compact && !strings.Contains(objMetadata.GroupKind.Group, compactGroup) { continue } @@ -137,6 +147,26 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus } ks := tree.Add(objMetadata) + + if objMetadata.GroupKind.Group == helmv2.GroupVersion.Group && + objMetadata.GroupKind.Kind == helmv2.HelmReleaseKind { + objects, err := getHelmReleaseInventory( + ctx, client.ObjectKey{ + Namespace: objMetadata.Namespace, + Name: objMetadata.Name, + }, kubeClient) + if err != nil { + return err + } + + for _, obj := range objects { + if compact && !strings.Contains(obj.GroupKind.Group, compactGroup) { + continue + } + ks.Add(obj) + } + } + if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind { k := &kustomizev1.Kustomization{} @@ -156,3 +186,84 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus return nil } + +type hrStorage struct { + Name string `json:"name,omitempty"` + Manifest string `json:"manifest,omitempty"` +} + +func getHelmReleaseInventory(ctx context.Context, objectKey client.ObjectKey, kubeClient client.Client) ([]object.ObjMetadata, error) { + hr := &helmv2.HelmRelease{} + if err := kubeClient.Get(ctx, objectKey, hr); err != nil { + return nil, err + } + + storageNamespace := hr.GetNamespace() + if hr.Spec.StorageNamespace != "" { + storageNamespace = hr.Spec.StorageNamespace + } + + storageName := hr.GetName() + if hr.Spec.ReleaseName != "" { + storageName = hr.Spec.ReleaseName + } else if hr.Spec.TargetNamespace != "" { + storageName = strings.Join([]string{hr.Spec.TargetNamespace, hr.Name}, "-") + } + + storageVersion := hr.Status.LastReleaseRevision + // skip release if it failed to install + if storageVersion < 1 { + return nil, nil + } + + storageKey := client.ObjectKey{ + Namespace: storageNamespace, + Name: fmt.Sprintf("sh.helm.release.v1.%s.v%v", storageName, storageVersion), + } + + storageSecret := &corev1.Secret{} + if err := kubeClient.Get(ctx, storageKey, storageSecret); err != nil { + // skip release if it has no storage + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to find the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) + } + + releaseData, releaseFound := storageSecret.Data["release"] + if !releaseFound { + return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s'", objectKey.String()) + } + + // adapted from https://github.com/helm/helm/blob/02685e94bd3862afcb44f6cd7716dbeb69743567/pkg/storage/driver/util.go + var b64 = base64.StdEncoding + b, err := b64.DecodeString(string(releaseData)) + if err != nil { + return nil, err + } + var magicGzip = []byte{0x1f, 0x8b, 0x08} + if bytes.Equal(b[0:3], magicGzip) { + r, err := gzip.NewReader(bytes.NewReader(b)) + if err != nil { + return nil, err + } + defer r.Close() + b2, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + b = b2 + } + + var rls hrStorage + if err := json.Unmarshal(b, &rls); err != nil { + return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) + } + + objects, err := ssa.ReadObjects(strings.NewReader(rls.Manifest)) + if err != nil { + return nil, fmt.Errorf("failed to read the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) + } + + return object.UnstructuredsToObjMetas(objects) +}