Skip to content

fleet cli json output #3559

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

Merged
merged 2 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cmd/fleetcli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
package main

import (
"os"
"strings"

// Ensure GVKs are registered
_ "github.com/rancher/fleet/pkg/generated/controllers/fleet.cattle.io"
_ "github.com/rancher/wrangler/v3/pkg/generated/controllers/apiextensions.k8s.io"
Expand All @@ -22,7 +25,16 @@ func main() {
ctx := signals.SetupSignalContext()
cmd := cmds.App()
if err := cmd.ExecuteContext(ctx); err != nil {
logrus.Fatal(err)
if strings.ToLower(os.Getenv(cmds.JSONOutputEnvVar)) == "true" {
log := logrus.New()
log.SetFormatter(&logrus.JSONFormatter{})
// use a fleet specific field name so we are sure logs from other libraries
// are not considered.
log.WithFields(logrus.Fields{
"fleetErrorMessage": err.Error(),
}).Fatal("Fleet cli failed")
} else {
logrus.Fatal(err)
}
}

}
11 changes: 8 additions & 3 deletions integrationtests/gitjob/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,15 @@ var _ = Describe("GitJob controller", func() {
job.Status.StartTime = &metav1.Time{Time: time.Now().Add(-1 * time.Hour)}
job.Status.Conditions = []batchv1.JobCondition{
{
Type: "Failed",
// using Stalled because the Compute function uses Stalled
// for returning the condition message and it's simpler.
// For testing it in a different way we would need to setup a more complex
// scenario defining the job pods
// We are simulating job failures.
Type: "Stalled",
Status: "True",
Reason: "BackoffLimitExceeded",
Message: "Job has reached the specified backoff limit",
Message: `{"fleetErrorMessage":"fleet error message","level":"fatal","msg":"Fleet cli failed","time":"2025-04-15T14:53:15+02:00"}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beware that message in status conditions is by definition meant to be human-readable:

❯ kubectl explain job.status.conditions.message
GROUP:      batch
KIND:       Job
VERSION:    v1

FIELD: message <string>


DESCRIPTION:
    Human readable message indicating details about last transition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for jobs it's okay and it allows much better errors in the UI?

Additionally:

JSON is a text-based, human-readable format for representing ...

},
{
Type: batchv1.JobFailureTarget,
Expand All @@ -566,7 +571,7 @@ var _ = Describe("GitJob controller", func() {
// check the conditions related to the job
// Failed.... Stalled=true and Reconcilling=false
g.Expect(checkCondition(&gitRepo, "Reconciling", corev1.ConditionFalse, "")).To(BeTrue())
g.Expect(checkCondition(&gitRepo, "Stalled", corev1.ConditionTrue, "Job Failed. failed: 1/1")).To(BeTrue())
g.Expect(checkCondition(&gitRepo, "Stalled", corev1.ConditionTrue, "fleet error message")).To(BeTrue())

// check the rest
g.Expect(checkCondition(&gitRepo, "GitPolling", corev1.ConditionTrue, "")).To(BeTrue())
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ var (
Client Getter
)

const JSONOutputEnvVar = "FLEET_JSON_OUTPUT"

func App() *cobra.Command {
root := command.Command(&Fleet{}, cobra.Command{
Version: version.FriendlyVersion(),
Expand Down
12 changes: 11 additions & 1 deletion internal/cmd/controller/gitops/reconciler/gitjob.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,13 @@ func (r *GitJobReconciler) newGitCloner(
args = append(args, "--ca-bundle-file", "/gitjob/cabundle/"+bundleCAFile)
}

env := proxyEnvVars()
env := []corev1.EnvVar{
{
Name: fleetcli.JSONOutputEnvVar,
Value: "true",
},
}
env = append(env, proxyEnvVars()...)

// If strict host key checks are enabled but no entries are available, another error will be shown by the known
// hosts getter, as that means that the Fleet deployment is incomplete.
Expand Down Expand Up @@ -639,6 +645,10 @@ func argsAndEnvs(
Name: "HOME",
Value: fleetHomeDir,
},
{
Name: fleetcli.JSONOutputEnvVar,
Value: "true",
},
{
Name: fleetcli.FleetApplyConflictRetriesEnv,
Value: strconv.Itoa(fleetApplyRetries),
Expand Down
59 changes: 58 additions & 1 deletion internal/cmd/controller/gitops/reconciler/gitjob_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package reconciler

import (
"context"
"encoding/json"
"fmt"
"math/rand/v2"
"sort"
Expand Down Expand Up @@ -562,7 +563,7 @@ func setStatusFromGitjob(ctx context.Context, c client.Client, gitRepo *v1alpha1
// - Terminating
switch result.Status {
case status.FailedStatus:
kstatus.SetError(gitRepo, terminationMessage)
kstatus.SetError(gitRepo, filterFleetCLIJobOutput(terminationMessage))
case status.CurrentStatus:
if strings.Contains(result.Message, "Job Completed") {
gitRepo.Status.Commit = job.Annotations["commit"]
Expand Down Expand Up @@ -654,3 +655,59 @@ func updateStatus(ctx context.Context, c client.Client, req types.NamespacedName
return nil
})
}

func filterFleetCLIJobOutput(output string) string {
// first split the output in lines
lines := strings.Split(output, "\n")
s := ""
for _, l := range lines {
s = s + getFleetCLIErrorsFromLine(l)
}

s = strings.Trim(s, "\n")
// in the case that all the messages from fleet apply are from libraries
// we just report an unknown error
if s == "" {
s = "Unknown error"
}

return s
}

func getFleetCLIErrorsFromLine(l string) string {
type LogEntry struct {
Level string `json:"level"`
FleetErrorMsg string `json:"fleetErrorMessage"`
Time string `json:"time"`
Msg string `json:"msg"`
}
s := ""
open := strings.IndexByte(l, '{')
if open == -1 {
// line does not contain a valid json string
return ""
}
close := strings.IndexByte(l, '}')
if close != -1 {
if close < open {
// looks like there is some garbage before a possible json string
// ignore everything up to that closing bracked and try again
return getFleetCLIErrorsFromLine(l[close+1:])
}
} else if close == -1 {
// line does not contain a valid json string
return ""
}
var entry LogEntry
if err := json.Unmarshal([]byte(l[open:close+1]), &entry); err == nil {
if entry.FleetErrorMsg != "" {
s = s + entry.FleetErrorMsg + "\n"
}
}
// check if there's more to parse
if close+1 < len(l) {
s = s + getFleetCLIErrorsFromLine(l[close+1:])
}

return s
}
Loading
Loading