-
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 all 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,146 @@ | ||
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{} | ||
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. we could probs use |
||
|
||
for _, entry := range objects { | ||
idstr := strings.Join([]string{entry.GetAPIVersion(), entry.GetKind()}, "_") | ||
|
||
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_test | ||||
|
||||
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.
This is the opposite of how our team has been structuring packages to this point. I don't really care either way, but we need to decide as an eng org how we want to do this.
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.
It's probably a per-codebase decision. It's not the end of the world to white box test some layers, but it is not great practice when it comes to external facing contracts (which these are). Given we are tidying things, now feels like a good time to clean this up.