diff --git a/cmd/capacitor/main.go b/cmd/capacitor/main.go index 86ac9f0..86b9cf0 100644 --- a/cmd/capacitor/main.go +++ b/cmd/capacitor/main.go @@ -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) diff --git a/deploy/k8s/rbac.yaml b/deploy/k8s/rbac.yaml index d1e7c28..309aeb9 100644 --- a/deploy/k8s/rbac.yaml +++ b/deploy/k8s/rbac.yaml @@ -34,6 +34,8 @@ rules: - gitrepositories - ocirepositories - buckets + - helmrepositories + - helmcharts - kustomizations - helmreleases verbs: diff --git a/pkg/controllers/helmChartController.go b/pkg/controllers/helmChartController.go new file mode 100644 index 0000000..0e8e9d1 --- /dev/null +++ b/pkg/controllers/helmChartController.go @@ -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 + }) +} diff --git a/pkg/controllers/helmRepositoryController.go b/pkg/controllers/helmRepositoryController.go new file mode 100644 index 0000000..a6d92bd --- /dev/null +++ b/pkg/controllers/helmRepositoryController.go @@ -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 + }) +} diff --git a/pkg/flux/flux.go b/pkg/flux/flux.go index 259c08c..f0e1bfc 100644 --- a/pkg/flux/flux.go +++ b/pkg/flux/flux.go @@ -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) { @@ -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). @@ -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 diff --git a/pkg/flux/reconcile.go b/pkg/flux/reconcile.go index 96b587d..c3a6961 100644 --- a/pkg/flux/reconcile.go +++ b/pkg/flux/reconcile.go @@ -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 diff --git a/pkg/flux/source.go b/pkg/flux/source.go index 991db6e..108effa 100644 --- a/pkg/flux/source.go +++ b/pkg/flux/source.go @@ -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) +} diff --git a/pkg/flux/types.go b/pkg/flux/types.go index 8149cbd..fade128 100644 --- a/pkg/flux/types.go +++ b/pkg/flux/types.go @@ -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 { diff --git a/web/src/ExpandedFooter.jsx b/web/src/ExpandedFooter.jsx index a6717ed..b11d292 100644 --- a/web/src/ExpandedFooter.jsx +++ b/web/src/ExpandedFooter.jsx @@ -39,7 +39,7 @@ export function ExpandedFooter(props) { } {selected === "Sources" && - + } {selected === "Flux Runtime" && diff --git a/web/src/Footer.js b/web/src/Footer.js index 881a5e0..819013a 100644 --- a/web/src/Footer.js +++ b/web/src/Footer.js @@ -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]); diff --git a/web/src/HelmChartWidget.jsx b/web/src/HelmChartWidget.jsx new file mode 100644 index 0000000..a1b77ff --- /dev/null +++ b/web/src/HelmChartWidget.jsx @@ -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 ( + <> + +
{source.spec.chart}@{revision} ({`${source.metadata.namespace}/${sourceRef.name}`})
+
+ + ) +} diff --git a/web/src/HelmRepositoryWidget.jsx b/web/src/HelmRepositoryWidget.jsx new file mode 100644 index 0000000..ef6c8bb --- /dev/null +++ b/web/src/HelmRepositoryWidget.jsx @@ -0,0 +1,11 @@ +import { NavigationButton } from './NavigationButton' + +export function HelmRepositoryWidget(props) { + const { source } = props + + return ( + <> + {source.spec.url} + + ) +} diff --git a/web/src/HelmRevisionWidget.jsx b/web/src/HelmRevisionWidget.jsx index a474dcc..a231003 100644 --- a/web/src/HelmRevisionWidget.jsx +++ b/web/src/HelmRevisionWidget.jsx @@ -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 @@ -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 && @@ -40,7 +45,9 @@ export function HelmRevisionWidget(props) { } Currently Installed: - {appliedRevision}@{version && version.chartName} + + {appliedRevision}@{version && version.chartName} + {withHistory &&
@@ -64,9 +71,9 @@ export function HelmRevisionWidget(props) {

{release.chartVersion}@{release.chartName} {statusLabel} - ago + ago {release.status === "superseded" && - now superseded + , now superseded }

) diff --git a/web/src/ReadyWidget.jsx b/web/src/ReadyWidget.jsx index 572c22f..4cc5589 100644 --- a/web/src/ReadyWidget.jsx +++ b/web/src/ReadyWidget.jsx @@ -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" diff --git a/web/src/Source.jsx b/web/src/Source.jsx index 5e8574e..6019ebb 100644 --- a/web/src/Source.jsx +++ b/web/src/Source.jsx @@ -2,9 +2,11 @@ import React, { useState, useEffect, useRef } from 'react'; import { ReadyWidget } from './ReadyWidget' import { ArtifactWidget } from './ArtifactWidget'; import { OCIArtifactWidget } from './OCIArtifactWidget'; +import { HelmChartWidget } from './HelmChartWidget'; +import { HelmRepositoryWidget } from './HelmRepositoryWidget'; export function Source(props) { - const { capacitorClient, source, targetReference } = props; + const { capacitorClient, source, targetReference, handleNavigationSelect } = props; const ref = useRef(null); const [highlight, setHighlight] = useState(false) @@ -22,7 +24,7 @@ export function Source(props) {
@@ -45,6 +47,12 @@ export function Source(props) { {source.kind === 'Bucket' && Bucket (TODO) } + {source.kind === 'HelmRepository' && + + } + {source.kind === 'HelmChart' && + + }