-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement the GetHelmRelease endpoint #1534
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
package server | ||
|
||
import ( | ||
"bytes" | ||
"compress/gzip" | ||
"context" | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"strings" | ||
|
||
"github.com/fluxcd/helm-controller/api/v2beta1" | ||
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" | ||
"github.com/fluxcd/pkg/ssa" | ||
"github.com/weaveworks/weave-gitops/core/server/types" | ||
pb "github.com/weaveworks/weave-gitops/pkg/api/core" | ||
v1 "k8s.io/api/core/v1" | ||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
) | ||
|
||
func (cs *coreServer) ListHelmReleases(ctx context.Context, msg *pb.ListHelmReleasesRequest) (*pb.ListHelmReleasesResponse, error) { | ||
k8s, err := cs.k8s.Client(ctx) | ||
if err != nil { | ||
return nil, doClientError(err) | ||
} | ||
|
||
l := &helmv2.HelmReleaseList{} | ||
|
||
if err := list(ctx, k8s, temporarilyEmptyAppName, msg.Namespace, l); err != nil { | ||
return nil, err | ||
} | ||
|
||
var results []*pb.HelmRelease | ||
for _, helmRelease := range l.Items { | ||
results = append(results, types.HelmReleaseToProto(&helmRelease, []*pb.GroupVersionKind{})) | ||
} | ||
|
||
return &pb.ListHelmReleasesResponse{ | ||
HelmReleases: results, | ||
}, nil | ||
} | ||
|
||
func (cs *coreServer) GetHelmRelease(ctx context.Context, msg *pb.GetHelmReleaseRequest) (*pb.GetHelmReleaseResponse, error) { | ||
k8s, err := cs.k8s.Client(ctx) | ||
if err != nil { | ||
return nil, doClientError(err) | ||
} | ||
|
||
helmRelease := helmv2.HelmRelease{} | ||
|
||
if err = get(ctx, k8s, msg.Name, msg.Namespace, &helmRelease); err != nil { | ||
return nil, err | ||
} | ||
|
||
inventory, err := getHelmReleaseInventory(ctx, helmRelease, k8s) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &pb.GetHelmReleaseResponse{ | ||
HelmRelease: types.HelmReleaseToProto(&helmRelease, inventory), | ||
}, err | ||
} | ||
|
||
func getHelmReleaseInventory(ctx context.Context, helmRelease v2beta1.HelmRelease, k8s client.Client) ([]*pb.GroupVersionKind, error) { | ||
storageNamespace := helmRelease.GetNamespace() | ||
if helmRelease.Spec.StorageNamespace != "" { | ||
storageNamespace = helmRelease.Spec.StorageNamespace | ||
} | ||
|
||
storageName := helmRelease.GetName() | ||
if helmRelease.Spec.ReleaseName != "" { | ||
storageName = helmRelease.Spec.ReleaseName | ||
} | ||
|
||
storageVersion := helmRelease.Status.LastReleaseRevision | ||
if storageVersion < 1 { | ||
// skip release if it failed to install | ||
return nil, nil | ||
} | ||
|
||
storageSecret := v1.Secret{} | ||
secretName := fmt.Sprintf("sh.helm.release.v1.%s.v%v", storageName, storageVersion) | ||
|
||
if err := get(ctx, k8s, secretName, storageNamespace, &storageSecret); err != nil { | ||
return nil, err | ||
} | ||
|
||
releaseData, releaseFound := storageSecret.Data["release"] | ||
if !releaseFound { | ||
return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s'", helmRelease.Name) | ||
} | ||
|
||
byteData, err := base64.StdEncoding.DecodeString(string(releaseData)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var magicGzip = []byte{0x1f, 0x8b, 0x08} | ||
if bytes.Equal(byteData[0:3], magicGzip) { | ||
r, err := gzip.NewReader(bytes.NewReader(byteData)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
defer r.Close() | ||
|
||
uncompressedByteData, err := io.ReadAll(r) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
byteData = uncompressedByteData | ||
} | ||
|
||
storage := types.HelmReleaseStorage{} | ||
if err := json.Unmarshal(byteData, &storage); err != nil { | ||
return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s': %w", helmRelease.Name, err) | ||
} | ||
|
||
objects, err := ssa.ReadObjects(strings.NewReader(storage.Manifest)) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to read the Helm storage object for HelmRelease '%s': %w", helmRelease.Name, err) | ||
} | ||
|
||
var gvk []*pb.GroupVersionKind | ||
|
||
found := map[string]bool{} | ||
|
||
for _, entry := range objects { | ||
entry.GetAPIVersion() | ||
|
||
idstr := strings.Join([]string{entry.GetAPIVersion(), entry.GetKind()}, "_") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know why we call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no idea, and because I have no idea, I just used the same logic XD There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it hurt anything if you remove the first one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while testing it does not hurt, but I don't know if it does with real k8s objects coming from a real k8s cluster. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ack, makes sense, would be good to prove and remove at somepoint There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed... Why?
The function call sequence is:
With that extra call the difference is an extra call only:
I see only one case when it can have any effects, if the underlying code is far from optimal and has an internal counter (how many times it was called) or something, and it does not make sense. |
||
|
||
if !found[idstr] { | ||
found[idstr] = true | ||
|
||
gvk = append(gvk, &pb.GroupVersionKind{ | ||
Group: entry.GroupVersionKind().Group, | ||
Version: entry.GroupVersionKind().Version, | ||
Kind: entry.GroupVersionKind().Kind, | ||
}) | ||
} | ||
} | ||
|
||
return gvk, nil | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,109 @@ | ||||
package server | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm 100% on the that side (blackbox testing), but the rest of the code follows this logic (whitebox testing), so I did the same. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeh. i would be good to move over while we are starting new so we don;t blur lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did a few pokes on it, all the tests are using the same functions (and global variable 😞 ), so I moved everything and now it's all |
||||
|
||||
import ( | ||||
"context" | ||||
"testing" | ||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" | ||||
. "github.com/onsi/gomega" | ||||
pb "github.com/weaveworks/weave-gitops/pkg/api/core" | ||||
"github.com/weaveworks/weave-gitops/pkg/kube" | ||||
"k8s.io/apimachinery/pkg/util/rand" | ||||
"sigs.k8s.io/controller-runtime/pkg/client" | ||||
) | ||||
|
||||
func TestListHelmReleases(t *testing.T) { | ||||
g := NewGomegaWithT(t) | ||||
ctx := context.Background() | ||||
|
||||
c, cleanup := makeGRPCServer(k8sEnv.Rest, t) | ||||
defer cleanup() | ||||
|
||||
_, k, err := kube.NewKubeHTTPClientWithConfig(k8sEnv.Rest, "") | ||||
g.Expect(err).NotTo(HaveOccurred()) | ||||
|
||||
appName := "myapp" | ||||
ns := newNamespace(ctx, k, g) | ||||
|
||||
newHelmRelease(ctx, appName, ns.Name, k, g) | ||||
|
||||
res, err := c.ListHelmReleases(ctx, &pb.ListHelmReleasesRequest{ | ||||
Namespace: ns.Name, | ||||
}) | ||||
g.Expect(err).NotTo(HaveOccurred()) | ||||
g.Expect(res.HelmReleases).To(HaveLen(1)) | ||||
g.Expect(res.HelmReleases[0].Name).To(Equal(appName)) | ||||
} | ||||
|
||||
func TestGetHelmRelease(t *testing.T) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are you going to test any of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah just literally saw your comment on that 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @yitsushi Here is how I had to test statuses: weave-gitops/core/server/automations_test.go Line 110 in 0fc37a2
Note that this is the second There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My issue with testing that function... well I have to create a full secret + store + data + "i don't know what else", because unlike kustomization, to get helm inventory, we query the api to get a helm secret, get release from the secret storage, decode + unzip (if gzipped), and finally read object from the storage. or something like that... so that would require a huge bootstrapping. I'll try it, because I would like to test it. |
||||
g := NewGomegaWithT(t) | ||||
ctx := context.Background() | ||||
|
||||
c, cleanup := makeGRPCServer(k8sEnv.Rest, t) | ||||
defer cleanup() | ||||
|
||||
_, k, err := kube.NewKubeHTTPClientWithConfig(k8sEnv.Rest, "") | ||||
g.Expect(err).NotTo(HaveOccurred()) | ||||
|
||||
appName := "myapp" + rand.String(5) | ||||
ns1 := newNamespace(ctx, k, g) | ||||
ns2 := newNamespace(ctx, k, g) | ||||
ns3 := newNamespace(ctx, k, g) | ||||
|
||||
newHelmRelease(ctx, appName, ns1.Name, k, g) | ||||
newHelmRelease(ctx, appName, ns2.Name, k, g) | ||||
|
||||
// Get app from ns1. | ||||
response, err := c.GetHelmRelease(ctx, &pb.GetHelmReleaseRequest{ | ||||
Name: appName, | ||||
Namespace: ns1.Name, | ||||
}) | ||||
|
||||
g.Expect(err).NotTo(HaveOccurred()) | ||||
g.Expect(response.HelmRelease.Name).To(Equal(appName)) | ||||
g.Expect(response.HelmRelease.Namespace).To(Equal(ns1.Name)) | ||||
|
||||
// Get app from ns2. | ||||
response, err = c.GetHelmRelease(ctx, &pb.GetHelmReleaseRequest{ | ||||
Name: appName, | ||||
Namespace: ns2.Name, | ||||
}) | ||||
|
||||
g.Expect(err).NotTo(HaveOccurred()) | ||||
g.Expect(response.HelmRelease.Name).To(Equal(appName)) | ||||
g.Expect(response.HelmRelease.Namespace).To(Equal(ns2.Name)) | ||||
|
||||
// Get app from ns3, should fail. | ||||
_, err = c.GetHelmRelease(ctx, &pb.GetHelmReleaseRequest{ | ||||
Name: appName, | ||||
Namespace: ns3.Name, | ||||
}) | ||||
|
||||
g.Expect(err).To(HaveOccurred()) | ||||
} | ||||
|
||||
func newHelmRelease( | ||||
ctx context.Context, | ||||
appName, nsName string, | ||||
k client.Client, | ||||
g *GomegaWithT, | ||||
) helmv2.HelmRelease { | ||||
release := helmv2.HelmRelease{ | ||||
Spec: helmv2.HelmReleaseSpec{ | ||||
Chart: helmv2.HelmChartTemplate{ | ||||
Spec: helmv2.HelmChartTemplateSpec{ | ||||
SourceRef: helmv2.CrossNamespaceObjectReference{ | ||||
Kind: "GitRepository", | ||||
Name: "somesource", | ||||
}, | ||||
}, | ||||
}, | ||||
}, | ||||
} | ||||
release.Name = appName | ||||
release.Namespace = nsName | ||||
|
||||
g.Expect(k.Create(ctx, &release)).To(Succeed()) | ||||
|
||||
return release | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could probs use
map[string]struct{}
here if we care about speed and memory, butbool
reads better so happy either way.