Skip to content

Commit 2a987b6

Browse files
committed
Generate Flux installation report
Signed-off-by: Stefan Prodan <[email protected]>
1 parent 60ca6ac commit 2a987b6

File tree

7 files changed

+433
-1
lines changed

7 files changed

+433
-1
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/opencontainers/go-digest v1.0.0
2222
github.com/otiai10/copy v1.14.0
2323
github.com/spf13/pflag v1.0.5
24+
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f
2425
k8s.io/api v0.30.1
2526
k8s.io/apiextensions-apiserver v0.30.1
2627
k8s.io/apimachinery v0.30.1
@@ -117,7 +118,6 @@ require (
117118
go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect
118119
go.uber.org/multierr v1.11.0 // indirect
119120
go.uber.org/zap v1.27.0 // indirect
120-
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
121121
golang.org/x/net v0.25.0 // indirect
122122
golang.org/x/oauth2 v0.19.0 // indirect
123123
golang.org/x/sync v0.7.0 // indirect

internal/reporter/components.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 Stefan Prodan.
2+
// SPDX-License-Identifier: AGPL-3.0
3+
4+
package reporter
5+
6+
import (
7+
"cmp"
8+
"context"
9+
"fmt"
10+
11+
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
12+
"golang.org/x/exp/slices"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
16+
fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
17+
)
18+
19+
func (r *FluxStatusReporter) getComponentsStatus(ctx context.Context) ([]fluxcdv1.FluxComponentStatus, error) {
20+
deployments := unstructured.UnstructuredList{
21+
Object: map[string]interface{}{
22+
"apiVersion": "apps/v1",
23+
"kind": "Deployment",
24+
},
25+
}
26+
27+
if err := r.List(ctx, &deployments, client.InNamespace(r.namespace), r.labelSelector); err != nil {
28+
return nil, fmt.Errorf("failed to list deployments: %w", err)
29+
}
30+
31+
components := make([]fluxcdv1.FluxComponentStatus, len(deployments.Items))
32+
for i, d := range deployments.Items {
33+
res, err := status.Compute(&d)
34+
if err != nil {
35+
components[i] = fluxcdv1.FluxComponentStatus{
36+
Name: d.GetName(),
37+
Ready: false,
38+
Status: fmt.Sprintf("Failed to compute status: %s", err.Error()),
39+
}
40+
} else {
41+
components[i] = fluxcdv1.FluxComponentStatus{
42+
Name: d.GetName(),
43+
Ready: res.Status == status.CurrentStatus,
44+
Status: fmt.Sprintf("%s %s", string(res.Status), res.Message),
45+
}
46+
}
47+
48+
containers, found, _ := unstructured.NestedSlice(d.Object, "spec", "template", "spec", "containers")
49+
if found && len(containers) > 0 {
50+
components[i].Image = containers[0].(map[string]interface{})["image"].(string)
51+
}
52+
}
53+
54+
slices.SortStableFunc(components, func(i, j fluxcdv1.FluxComponentStatus) int {
55+
return cmp.Compare(i.Name, j.Name)
56+
})
57+
58+
return components, nil
59+
}

internal/reporter/crds.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2024 Stefan Prodan.
2+
// SPDX-License-Identifier: AGPL-3.0
3+
4+
package reporter
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
11+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
)
15+
16+
func (r *FluxStatusReporter) listCRDs(ctx context.Context) ([]metav1.GroupVersionKind, error) {
17+
var list apiextensionsv1.CustomResourceDefinitionList
18+
if err := r.List(ctx, &list, client.InNamespace(""), r.labelSelector); err != nil {
19+
return nil, fmt.Errorf("failed to list CRDs: %w", err)
20+
}
21+
22+
if len(list.Items) == 0 {
23+
return nil, errors.New("no Flux CRDs found")
24+
}
25+
26+
gvkList := make([]metav1.GroupVersionKind, len(list.Items))
27+
for i, crd := range list.Items {
28+
gvk := metav1.GroupVersionKind{
29+
Group: crd.Spec.Group,
30+
Kind: crd.Spec.Names.Kind,
31+
}
32+
versions := crd.Status.StoredVersions
33+
if len(versions) > 0 {
34+
gvk.Version = versions[len(versions)-1]
35+
} else {
36+
return nil, fmt.Errorf("no stored versions found for CRD %s", crd.Name)
37+
}
38+
gvkList[i] = gvk
39+
}
40+
41+
return gvkList, nil
42+
}
43+
44+
func gvkFor(kind string, crds []metav1.GroupVersionKind) *metav1.GroupVersionKind {
45+
for _, gvk := range crds {
46+
if gvk.Kind == kind {
47+
return &gvk
48+
}
49+
}
50+
return nil
51+
}

internal/reporter/distribution.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2024 Stefan Prodan.
2+
// SPDX-License-Identifier: AGPL-3.0
3+
4+
package reporter
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
15+
fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
16+
"github.com/controlplaneio-fluxcd/flux-operator/internal/entitlement"
17+
)
18+
19+
func (r *FluxStatusReporter) getDistributionStatus(ctx context.Context) fluxcdv1.FluxDistributionStatus {
20+
result := fluxcdv1.FluxDistributionStatus{
21+
Status: "Unknown",
22+
Entitlement: "Unknown",
23+
}
24+
25+
crdMeta := &metav1.PartialObjectMetadata{
26+
TypeMeta: metav1.TypeMeta{
27+
APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
28+
Kind: "CustomResourceDefinition",
29+
},
30+
ObjectMeta: metav1.ObjectMeta{
31+
Name: "gitrepositories.source.toolkit.fluxcd.io",
32+
},
33+
}
34+
if err := r.Get(ctx, client.ObjectKeyFromObject(crdMeta), crdMeta); err == nil {
35+
result.Status = "Installed"
36+
37+
if version, found := crdMeta.Labels["app.kubernetes.io/version"]; found {
38+
result.Version = version
39+
}
40+
41+
if manager, ok := crdMeta.Labels["app.kubernetes.io/managed-by"]; ok {
42+
result.ManagedBy = manager
43+
} else if _, ok := crdMeta.Labels["kustomize.toolkit.fluxcd.io/name"]; ok {
44+
result.ManagedBy = "flux bootstrap"
45+
}
46+
} else {
47+
result.Status = "Not Installed"
48+
}
49+
50+
entitlementSecret := &corev1.Secret{}
51+
err := r.Get(ctx, client.ObjectKey{
52+
Namespace: r.namespace,
53+
Name: fmt.Sprintf("%s-entitlement", r.manager),
54+
}, entitlementSecret)
55+
if err == nil {
56+
if _, found := entitlementSecret.Data[entitlement.TokenKey]; found {
57+
result.Entitlement = "Issued"
58+
if vendor, found := entitlementSecret.Data[entitlement.VendorKey]; found {
59+
result.Entitlement += " by " + string(vendor)
60+
}
61+
}
62+
}
63+
64+
return result
65+
}

internal/reporter/reconcilers.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2024 Stefan Prodan.
2+
// SPDX-License-Identifier: AGPL-3.0
3+
4+
package reporter
5+
6+
import (
7+
"cmp"
8+
"context"
9+
"fmt"
10+
11+
"github.com/fluxcd/cli-utils/pkg/kstatus/status"
12+
"github.com/fluxcd/pkg/apis/meta"
13+
"golang.org/x/exp/slices"
14+
corev1 "k8s.io/api/core/v1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
kerrors "k8s.io/apimachinery/pkg/util/errors"
18+
"sigs.k8s.io/controller-runtime/pkg/client"
19+
20+
fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
21+
)
22+
23+
func (r *FluxStatusReporter) getReconcilersStatus(ctx context.Context, crds []metav1.GroupVersionKind) ([]fluxcdv1.FluxReconcilerStatus, error) {
24+
var multiErr error
25+
resStats := make([]fluxcdv1.FluxReconcilerStatus, len(crds))
26+
for i, gvk := range crds {
27+
var total int
28+
var suspended int
29+
var failing int
30+
var totalSize int64
31+
32+
list := unstructured.UnstructuredList{
33+
Object: map[string]interface{}{
34+
"apiVersion": gvk.Group + "/" + gvk.Version,
35+
"kind": gvk.Kind,
36+
},
37+
}
38+
39+
if err := r.List(ctx, &list, client.InNamespace("")); err == nil {
40+
total = len(list.Items)
41+
42+
for _, item := range list.Items {
43+
if s, _, _ := unstructured.NestedBool(item.Object, "spec", "suspend"); s {
44+
suspended++
45+
}
46+
47+
if obj, err := status.GetObjectWithConditions(item.Object); err == nil {
48+
for _, cond := range obj.Status.Conditions {
49+
if cond.Type == meta.ReadyCondition && cond.Status == corev1.ConditionFalse {
50+
failing++
51+
}
52+
}
53+
}
54+
55+
if size, found, _ := unstructured.NestedInt64(item.Object, "status", "artifact", "size"); found {
56+
totalSize += size
57+
}
58+
}
59+
} else {
60+
multiErr = kerrors.NewAggregate([]error{multiErr, err})
61+
}
62+
63+
resStats[i] = fluxcdv1.FluxReconcilerStatus{
64+
APIVersion: gvk.Group + "/" + gvk.Version,
65+
Kind: gvk.Kind,
66+
Stats: fluxcdv1.FluxReconcilerStats{
67+
Running: total - suspended,
68+
Failing: failing,
69+
Suspended: suspended,
70+
TotalSize: formatSize(totalSize),
71+
},
72+
}
73+
}
74+
75+
slices.SortStableFunc(resStats, func(i, j fluxcdv1.FluxReconcilerStatus) int {
76+
return cmp.Compare(i.APIVersion+i.Kind, j.APIVersion+j.Kind)
77+
})
78+
79+
return resStats, multiErr
80+
}
81+
82+
func formatSize(b int64) string {
83+
if b == 0 {
84+
return ""
85+
}
86+
const unit = 1024
87+
if b < unit {
88+
return fmt.Sprintf("%d B", b)
89+
}
90+
div, exp := int64(unit), 0
91+
for n := b / unit; n >= unit; n /= unit {
92+
div *= unit
93+
exp++
94+
}
95+
return fmt.Sprintf("%.1f %ciB",
96+
float64(b)/float64(div), "KMGTPE"[exp])
97+
}

internal/reporter/reporter.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2024 Stefan Prodan.
2+
// SPDX-License-Identifier: AGPL-3.0
3+
4+
package reporter
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
fluxcdv1 "github.com/controlplaneio-fluxcd/flux-operator/api/v1"
13+
)
14+
15+
// FluxStatusReporter is responsible for computing
16+
// the status report of the Flux installation.
17+
type FluxStatusReporter struct {
18+
client.Client
19+
20+
instance string
21+
manager string
22+
namespace string
23+
labelSelector client.MatchingLabels
24+
}
25+
26+
// NewFluxStatusReporter creates a new FluxStatusReporter
27+
// for the given instance and namespace.
28+
func NewFluxStatusReporter(kubeClient client.Client, instance, manager, namespace string) *FluxStatusReporter {
29+
return &FluxStatusReporter{
30+
Client: kubeClient,
31+
instance: instance,
32+
manager: manager,
33+
namespace: namespace,
34+
labelSelector: client.MatchingLabels{"app.kubernetes.io/part-of": instance},
35+
}
36+
}
37+
38+
// Compute generate the status report of the Flux installation.
39+
func (r *FluxStatusReporter) Compute(ctx context.Context) (fluxcdv1.FluxReportSpec, error) {
40+
report := fluxcdv1.FluxReportSpec{}
41+
report.Distribution = r.getDistributionStatus(ctx)
42+
43+
crds, err := r.listCRDs(ctx)
44+
if err != nil {
45+
return report, fmt.Errorf("failed to list CRDs: %w", err)
46+
}
47+
48+
componentsStatus, err := r.getComponentsStatus(ctx)
49+
if err != nil {
50+
return report, fmt.Errorf("failed to compute components status: %w", err)
51+
}
52+
report.ComponentsStatus = componentsStatus
53+
54+
reconcilersStatus, err := r.getReconcilersStatus(ctx, crds)
55+
if err != nil {
56+
return report, fmt.Errorf("failed to compute reconcilers status: %w", err)
57+
}
58+
report.ReconcilersStatus = reconcilersStatus
59+
60+
syncStatus, err := r.getSyncStatus(ctx, crds)
61+
if err != nil {
62+
return report, fmt.Errorf("failed to compute sync status: %w", err)
63+
}
64+
report.SyncStatus = syncStatus
65+
66+
return report, nil
67+
}

0 commit comments

Comments
 (0)