Skip to content

Commit

Permalink
Enhancement: HelmRepositories and HelmCharts under "Sources" tab (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzsak authored Apr 8, 2024
1 parent f369dfe commit 521fe0f
Show file tree
Hide file tree
Showing 17 changed files with 286 additions and 22 deletions.
4 changes: 4 additions & 0 deletions cmd/capacitor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func main() {
runController(err, ociRepositoryController, stopCh)
bucketController, err := controllers.BucketController(client, dynamicClient, clientHub)
runController(err, bucketController, stopCh)
helmRepositoryController, err := controllers.HelmRepositoryController(client, dynamicClient, clientHub)
runController(err, helmRepositoryController, stopCh)
helmChartController, err := controllers.HelmChartController(client, dynamicClient, clientHub)
runController(err, helmChartController, stopCh)
kustomizationController, err := controllers.KustomizeController(client, dynamicClient, clientHub)
runController(err, kustomizationController, stopCh)
helmReleaseController, err := controllers.HelmReleaseController(client, dynamicClient, clientHub)
Expand Down
2 changes: 2 additions & 0 deletions deploy/k8s/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ rules:
- gitrepositories
- ocirepositories
- buckets
- helmrepositories
- helmcharts
- kustomizations
- helmreleases
verbs:
Expand Down
54 changes: 54 additions & 0 deletions pkg/controllers/helmChartController.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package controllers

import (
"encoding/json"

"github.com/gimlet-io/capacitor/pkg/flux"
"github.com/gimlet-io/capacitor/pkg/streaming"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

var helmChartResource = schema.GroupVersionResource{
Group: "source.toolkit.fluxcd.io",
Version: "v1beta2",
Resource: "helmcharts",
}

func HelmChartController(
client *kubernetes.Clientset,
dynamicClient *dynamic.DynamicClient,
clientHub *streaming.ClientHub,
) (*Controller, error) {
return NewDynamicController(
"helmcharts.source.toolkit.fluxcd.io",
dynamicClient,
helmChartResource,
func(informerEvent Event, objectMeta metav1.ObjectMeta, obj interface{}) error {
switch informerEvent.eventType {
case "create":
fallthrough
case "update":
fallthrough
case "delete":
fluxState, err := flux.State(client, dynamicClient)
if err != nil {
logrus.Warnf("could not get flux state: %s", err)
return nil
}
fluxStateBytes, err := json.Marshal(streaming.Envelope{
Type: streaming.FLUX_STATE_RECEIVED,
Payload: fluxState,
})
if err != nil {
logrus.Warnf("could not marshal event: %s", err)
return nil
}
clientHub.Broadcast <- fluxStateBytes
}
return nil
})
}
54 changes: 54 additions & 0 deletions pkg/controllers/helmRepositoryController.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package controllers

import (
"encoding/json"

"github.com/gimlet-io/capacitor/pkg/flux"
"github.com/gimlet-io/capacitor/pkg/streaming"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

var helmRepositoryResource = schema.GroupVersionResource{
Group: "source.toolkit.fluxcd.io",
Version: "v1beta2",
Resource: "helmrepositories",
}

func HelmRepositoryController(
client *kubernetes.Clientset,
dynamicClient *dynamic.DynamicClient,
clientHub *streaming.ClientHub,
) (*Controller, error) {
return NewDynamicController(
"helmrepositories.source.toolkit.fluxcd.io",
dynamicClient,
helmRepositoryResource,
func(informerEvent Event, objectMeta metav1.ObjectMeta, obj interface{}) error {
switch informerEvent.eventType {
case "create":
fallthrough
case "update":
fallthrough
case "delete":
fluxState, err := flux.State(client, dynamicClient)
if err != nil {
logrus.Warnf("could not get flux state: %s", err)
return nil
}
fluxStateBytes, err := json.Marshal(streaming.Envelope{
Type: streaming.FLUX_STATE_RECEIVED,
Payload: fluxState,
})
if err != nil {
logrus.Warnf("could not marshal event: %s", err)
return nil
}
clientHub.Broadcast <- fluxStateBytes
}
return nil
})
}
58 changes: 52 additions & 6 deletions pkg/flux/flux.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ var (
Version: "v2beta1",
Resource: "helmreleases",
}

helmRepositoryGVR = schema.GroupVersionResource{
Group: "source.toolkit.fluxcd.io",
Version: "v1beta2",
Resource: "helmrepositories",
}

helmChartGVR = schema.GroupVersionResource{
Group: "source.toolkit.fluxcd.io",
Version: "v1beta2",
Resource: "helmcharts",
}
)

func helmServices(dc *dynamic.DynamicClient) ([]Service, error) {
Expand Down Expand Up @@ -336,12 +348,14 @@ func helmStatusWithResources(

func State(c *kubernetes.Clientset, dc *dynamic.DynamicClient) (*FluxState, error) {
fluxState := &FluxState{
GitRepositories: []sourcev1.GitRepository{},
OCIRepositories: []sourcev1beta2.OCIRepository{},
Buckets: []sourcev1beta2.Bucket{},
Kustomizations: []kustomizationv1.Kustomization{},
HelmReleases: []helmv2beta2.HelmRelease{},
FluxServices: []Service{},
GitRepositories: []sourcev1.GitRepository{},
OCIRepositories: []sourcev1beta2.OCIRepository{},
Buckets: []sourcev1beta2.Bucket{},
Kustomizations: []kustomizationv1.Kustomization{},
HelmReleases: []helmv2beta2.HelmRelease{},
HelmRepositories: []sourcev1beta2.HelmRepository{},
HelmCharts: []sourcev1beta2.HelmChart{},
FluxServices: []Service{},
}

gitRepositories, err := dc.Resource(gitRepositoryGVR).
Expand Down Expand Up @@ -442,6 +456,38 @@ func State(c *kubernetes.Clientset, dc *dynamic.DynamicClient) (*FluxState, erro
fluxState.HelmReleases = append(fluxState.HelmReleases, helmRelease)
}

helmRepositories, err := dc.Resource(helmRepositoryGVR).
Namespace("").
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, h := range helmRepositories.Items {
unstructured := h.UnstructuredContent()
var helmRepository sourcev1beta2.HelmRepository
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &helmRepository)
if err != nil {
return nil, err
}
fluxState.HelmRepositories = append(fluxState.HelmRepositories, helmRepository)
}

helmCharts, err := dc.Resource(helmChartGVR).
Namespace("").
List(context.TODO(), metav1.ListOptions{})
if err != nil {
return nil, err
}
for _, h := range helmCharts.Items {
unstructured := h.UnstructuredContent()
var helmChart sourcev1beta2.HelmChart
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, &helmChart)
if err != nil {
return nil, err
}
fluxState.HelmCharts = append(fluxState.HelmCharts, helmChart)
}

fluxServices, err := fluxServicesWithDetails(c)
if err != nil {
return nil, err
Expand Down
12 changes: 12 additions & 0 deletions pkg/flux/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ func NewReconcileCommand(resource string) *reconcileCommand {
groupVersion: sourcev1beta2.GroupVersion,
kind: sourcev1beta2.BucketKind,
}
case sourcev1beta2.HelmRepositoryKind:
return &reconcileCommand{
object: helmRepositoryAdapter{&sourcev1beta2.HelmRepository{}},
groupVersion: sourcev1beta2.GroupVersion,
kind: sourcev1beta2.HelmRepositoryKind,
}
case sourcev1beta2.HelmChartKind:
return &reconcileCommand{
object: helmChartAdapter{&sourcev1beta2.HelmChart{}},
groupVersion: sourcev1beta2.GroupVersion,
kind: sourcev1beta2.HelmChartKind,
}
}

return nil
Expand Down
40 changes: 40 additions & 0 deletions pkg/flux/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,43 @@ func (obj bucketAdapter) lastHandledReconcileRequest() string {
func (obj bucketAdapter) successMessage() string {
return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision)
}

type helmRepositoryAdapter struct {
*sourcev1beta2.HelmRepository
}

func (a helmRepositoryAdapter) asClientObject() client.Object {
return a.HelmRepository
}

func (obj helmRepositoryAdapter) isSuspended() bool {
return obj.HelmRepository.Spec.Suspend
}

func (obj helmRepositoryAdapter) lastHandledReconcileRequest() string {
return obj.Status.GetLastHandledReconcileRequest()
}

func (obj helmRepositoryAdapter) successMessage() string {
return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision)
}

type helmChartAdapter struct {
*sourcev1beta2.HelmChart
}

func (a helmChartAdapter) asClientObject() client.Object {
return a.HelmChart
}

func (obj helmChartAdapter) isSuspended() bool {
return obj.HelmChart.Spec.Suspend
}

func (obj helmChartAdapter) lastHandledReconcileRequest() string {
return obj.Status.GetLastHandledReconcileRequest()
}

func (obj helmChartAdapter) successMessage() string {
return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision)
}
14 changes: 8 additions & 6 deletions pkg/flux/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
)

type FluxState struct {
GitRepositories []sourcev1.GitRepository `json:"gitRepositories"`
OCIRepositories []sourcev1beta2.OCIRepository `json:"ociRepositories"`
Buckets []sourcev1beta2.Bucket `json:"buckets"`
Kustomizations []kustomizationv1.Kustomization `json:"kustomizations"`
HelmReleases []helmv2beta2.HelmRelease `json:"helmReleases"`
FluxServices []Service `json:"fluxServices"`
GitRepositories []sourcev1.GitRepository `json:"gitRepositories"`
OCIRepositories []sourcev1beta2.OCIRepository `json:"ociRepositories"`
Buckets []sourcev1beta2.Bucket `json:"buckets"`
Kustomizations []kustomizationv1.Kustomization `json:"kustomizations"`
HelmReleases []helmv2beta2.HelmRelease `json:"helmReleases"`
HelmRepositories []sourcev1beta2.HelmRepository `json:"helmRepositories"`
HelmCharts []sourcev1beta2.HelmChart `json:"helmCharts"`
FluxServices []Service `json:"fluxServices"`
}

type Service struct {
Expand Down
2 changes: 1 addition & 1 deletion web/src/ExpandedFooter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function ExpandedFooter(props) {
<HelmReleases capacitorClient={client} helmReleases={fluxState.helmReleases} targetReference={targetReference} handleNavigationSelect={handleNavigationSelect} />
}
{selected === "Sources" &&
<Sources capacitorClient={client} fluxState={fluxState} targetReference={targetReference} />
<Sources capacitorClient={client} fluxState={fluxState} targetReference={targetReference} handleNavigationSelect={handleNavigationSelect} />
}
{selected === "Flux Runtime" &&
<CompactServices capacitorClient={client} store={store} services={fluxState.fluxServices} />
Expand Down
2 changes: 2 additions & 0 deletions web/src/Footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const Footer = memo(function Footer(props) {
sources.push(...fluxState.ociRepositories)
sources.push(...fluxState.gitRepositories)
sources.push(...fluxState.buckets)
sources.push(...fluxState.helmRepositories)
sources.push(...fluxState.helmCharts)
}
return [...sources].sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
}, [fluxState]);
Expand Down
19 changes: 19 additions & 0 deletions web/src/HelmChartWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { NavigationButton } from './NavigationButton'

export function HelmChartWidget(props) {
const { source, handleNavigationSelect } = props

const sourceRef = source.spec.sourceRef
const artifact = source.status.artifact
const revision = artifact.revision

const navigationHandler = () => handleNavigationSelect("Sources", source.metadata.namespace, sourceRef.name, sourceRef.kind)

return (
<>
<NavigationButton handleNavigation={navigationHandler}>
<div className='text-left'>{source.spec.chart}@{revision} ({`${source.metadata.namespace}/${sourceRef.name}`})</div>
</NavigationButton>
</>
)
}
11 changes: 11 additions & 0 deletions web/src/HelmRepositoryWidget.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NavigationButton } from './NavigationButton'

export function HelmRepositoryWidget(props) {
const { source } = props

return (
<>
{source.spec.url}
</>
)
}
15 changes: 11 additions & 4 deletions web/src/HelmRevisionWidget.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import jp from 'jsonpath'
import { format } from "date-fns";
import { TimeLabel } from './TimeLabel'
import { NavigationButton } from './NavigationButton'

export function HelmRevisionWidget(props) {
const { helmRelease, withHistory } = props
const { helmRelease, withHistory, handleNavigationSelect } = props

const version = helmRelease.status.history ? helmRelease.status.history[0] : undefined
const appliedRevision = helmRelease.status.lastAppliedRevision
Expand All @@ -23,6 +24,10 @@ export function HelmRevisionWidget(props) {
const reconcilingCondition = reconcilingConditions.length === 1 ? reconcilingConditions[0] : undefined
const reconciling = reconcilingCondition && reconcilingConditions[0].status === "True"

const sourceRef = helmRelease.spec.chart.spec.sourceRef
const namespace = sourceRef.namespace ? sourceRef.namespace : helmRelease.metadata.namespace
const navigationHandler = () => handleNavigationSelect("Sources", namespace, sourceRef.name, sourceRef.kind)

return (
<>
{!ready && reconciling && !stalled &&
Expand All @@ -40,7 +45,9 @@ export function HelmRevisionWidget(props) {
}
<span className={`block ${ready || reconciling ? '' : 'font-normal text-neutral-600'} field`}>
<span>Currently Installed: </span>
{appliedRevision}@{version && version.chartName}
<NavigationButton handleNavigation={navigationHandler}>
{appliedRevision}@{version && version.chartName}
</NavigationButton>
</span>
{withHistory &&
<div className='pt-1 text-sm'>
Expand All @@ -64,9 +71,9 @@ export function HelmRevisionWidget(props) {
<p key={`${release.chartVersion}@${release.chartName}:${release.digest}`} className={`${current ? "text-neutral-700" : "font-normal text-neutral-500"}`}>
<span>{release.chartVersion}@{release.chartName}</span>
<span className='pl-1'>{statusLabel}</span>
<TimeLabel title={exactDate} date={parsed} /> ago
<span className='pl-1'><TimeLabel title={exactDate} date={parsed} /> ago</span>
{release.status === "superseded" &&
<span className='pl-1'>now superseded</span>
<span>, now superseded</span>
}
</p>
)
Expand Down
2 changes: 1 addition & 1 deletion web/src/ReadyWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function ReadyWidget(props) {

var [color,statusLabel,messageColor] = ['','','']
const readyLabel = label ? label : "Ready"
if (resource.kind === 'GitRepository' || resource.kind === "OCIRepository" || resource.kind === "Bucket") {
if (resource.kind === 'GitRepository' || resource.kind === "OCIRepository" || resource.kind === "Bucket" || resource.kind === "HelmRepository" || resource.kind === "HelmChart") {
color = fetchFailed ? "bg-orange-400 animate-pulse" : reconciling ? "bg-blue-400 animate-pulse" : ready ? "bg-teal-400" : "bg-orange-400 animate-pulse"
statusLabel = fetchFailed ? "Error" : reconciling ? "Reconciling" : ready ? readyLabel : "Error"
messageColor = fetchFailed ? "bg-orange-400" : reconciling ? "text-neutral-600" : ready ? "text-neutral-600 field" : "bg-orange-400"
Expand Down
Loading

0 comments on commit 521fe0f

Please sign in to comment.