Skip to content

Commit

Permalink
Merge pull request #1534 from weaveworks/1498-get-helm-release-endpoint
Browse files Browse the repository at this point in the history
feat: Implement the GetHelmRelease endpoint
  • Loading branch information
yitsushi committed Mar 2, 2022
2 parents b7f6a21 + f63e2d5 commit 657e092
Show file tree
Hide file tree
Showing 15 changed files with 991 additions and 407 deletions.
21 changes: 21 additions & 0 deletions api/core/core.proto
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ service Core {
get : "/v1/kustomizations/{name}"
};
}

/*
* ListHelmReleases lists helm releases from a cluster.
*/
Expand All @@ -48,6 +49,17 @@ service Core {
};
}


/*
* GetHelmRelease gets data about a single HelmRelease from the cluster.
*/
rpc GetHelmRelease(GetHelmReleaseRequest) returns (GetHelmReleaseResponse) {
option (google.api.http) = {
get : "/v1/helmrelease/{name}"
};
}


// Sources

/*
Expand Down Expand Up @@ -143,6 +155,15 @@ message ListHelmReleasesResponse {
repeated HelmRelease helm_releases = 1;
}

message GetHelmReleaseRequest {
string name = 1;
string namespace = 2;
}

message GetHelmReleaseResponse {
HelmRelease helm_release = 1;
}

message ListGitRepositoriesRequest {
string namespace = 1;
}
Expand Down
45 changes: 45 additions & 0 deletions api/core/core.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,43 @@
]
}
},
"/v1/helmrelease/{name}": {
"get": {
"summary": "GetHelmRelease gets data about a single HelmRelease from the cluster.",
"operationId": "Core_GetHelmRelease",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1GetHelmReleaseResponse"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "name",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "namespace",
"in": "query",
"required": false,
"type": "string"
}
],
"tags": [
"Core"
]
}
},
"/v1/helmreleases": {
"get": {
"summary": "ListHelmReleases lists helm releases from a cluster.",
Expand Down Expand Up @@ -545,6 +582,14 @@
}
}
},
"v1GetHelmReleaseResponse": {
"type": "object",
"properties": {
"helmRelease": {
"$ref": "#/definitions/v1HelmRelease"
}
}
},
"v1GetKustomizationResponse": {
"type": "object",
"properties": {
Expand Down
7 changes: 4 additions & 3 deletions core/server/fluxruntime_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package server
package server_test

import (
"context"
"fmt"
"testing"

. "github.com/onsi/gomega"
"github.com/weaveworks/weave-gitops/core/server"
pb "github.com/weaveworks/weave-gitops/pkg/api/core"
"github.com/weaveworks/weave-gitops/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
Expand Down Expand Up @@ -33,8 +34,8 @@ func TestGetReconciledObjects(t *testing.T) {
Name: "my-deployment",
Namespace: ns.Name,
Labels: map[string]string{
KustomizeNameKey: automationName,
KustomizeNamespaceKey: ns.Name,
server.KustomizeNameKey: automationName,
server.KustomizeNamespaceKey: ns.Name,
},
},
Spec: appsv1.DeploymentSpec{
Expand Down
146 changes: 146 additions & 0 deletions core/server/helm_release.go
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{}

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
}
109 changes: 109 additions & 0 deletions core/server/helm_release_test.go
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) {
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
}

0 comments on commit 657e092

Please sign in to comment.