Skip to content

Commit ba2de78

Browse files
Merge pull request #846 from rolandmkunkel/SREP-3640-add-dry-run-and-link-to-logs
SREP-3640: add dry run option and link to logs to output for CAD run …
2 parents ee9c257 + ab483d3 commit ba2de78

File tree

6 files changed

+334
-67
lines changed

6 files changed

+334
-67
lines changed

cmd/cluster/cad/run.go

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package cad
33
import (
44
"context"
55
"fmt"
6+
"slices"
67

8+
"github.com/openshift/osdctl/cmd/setup"
79
"github.com/openshift/osdctl/pkg/k8s"
810
"github.com/openshift/osdctl/pkg/utils"
911
"github.com/spf13/cobra"
12+
"github.com/spf13/viper"
1013
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1114
"k8s.io/apimachinery/pkg/runtime/schema"
1215
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -15,6 +18,8 @@ import (
1518
const (
1619
cadClusterIDProd = "2fbi9mjhqpobh20ot5d7e5eeq3a8gfhs" // These IDs are hard-coded in app-interface
1720
cadClusterIDStage = "2f9ghpikkv446iidcv7b92em2hgk13q9"
21+
cadNamespaceProd = "configuration-anomaly-detection-production"
22+
cadNamespaceStage = "configuration-anomaly-detection-stage"
1823
)
1924

2025
var validInvestigations = []string{
@@ -40,6 +45,7 @@ type cadRunOptions struct {
4045
investigation string
4146
elevationReason string
4247
environment string
48+
isDryRun bool
4349
}
4450

4551
func newCmdRun() *cobra.Command {
@@ -51,7 +57,7 @@ func newCmdRun() *cobra.Command {
5157
Long: `Run a manual investigation on the Configuration Anomaly Detection (CAD) cluster.
5258
5359
This command schedules a Tekton PipelineRun on the appropriate CAD cluster (stage or production)
54-
to run an investigation against a target cluster.
60+
to run an investigation against a target cluster. The results will be written to a backplane report.
5561
5662
Prerequisites:
5763
- Connected to the target cluster's OCM environment (production or stage)
@@ -61,17 +67,29 @@ Available Investigations:
6167
chgm, cmbb, can-not-retrieve-updates, ai, cpd, etcd-quota-low,
6268
insightsoperatordown, machine-health-check, must-gather, upgrade-config
6369
64-
Example:
65-
# Run a change management investigation on a production cluster
66-
osdctl cluster cad run \
67-
--cluster-id 1a2b3c4d5e6f7g8h9i0j \
68-
--investigation chgm \
69-
--environment production \
70-
--reason "OHSS-12345"
70+
Examples:
71+
` + "```bash" + `
72+
# Run a change management investigation on a production cluster
73+
osdctl cluster cad run \
74+
--cluster-id 1a2b3c4d5e6f7g8h9i0j \
75+
--investigation chgm \
76+
--environment production \
77+
--reason "OHSS-12345"
78+
79+
# Run a dry-run investigation (does not create a report)
80+
osdctl cluster cad run \
81+
--cluster-id 1a2b3c4d5e6f7g8h9i0j \
82+
--investigation chgm \
83+
--environment production \
84+
--reason "OHSS-12345" \
85+
--dry-run
86+
` + "```" + `
7187
7288
Note:
7389
After the investigation completes (may take several minutes), view results using:
74-
osdctl cluster reports list -C <cluster-id> -l 1
90+
` + "```bash" + `
91+
osdctl cluster reports list -C <cluster-id> -l 1
92+
` + "```" + `
7593
7694
You must be connected to the target cluster's OCM environment to view its reports.`,
7795
Args: cobra.NoArgs,
@@ -83,9 +101,15 @@ Note:
83101

84102
runCmd.Flags().StringVarP(&opts.clusterID, "cluster-id", "C", "", "Cluster ID (internal or external)")
85103
runCmd.Flags().StringVarP(&opts.investigation, "investigation", "i", "", "Investigation name")
86-
runCmd.Flags().StringVarP(&opts.environment, "environment", "e", "", "Environment of the cluster we want to run the investigation on. Allowed values: \"stage\" or \"production\"")
104+
runCmd.Flags().StringVarP(&opts.environment, "environment", "e", "", "Environment in which the target cluster runs. Allowed values: \"stage\" or \"production\"")
105+
runCmd.Flags().BoolVarP(&opts.isDryRun, "dry-run", "d", false, "Dry-Run: Run the investigation with the dry-run flag. This will not create a report.")
87106
runCmd.Flags().StringVar(&opts.elevationReason, "reason", "", "Provide a reason for running a manual investigation, used for backplane. Eg: 'OHSS-XXXX', or '#ITN-2024-XXXXX.")
88107

108+
_ = runCmd.MarkFlagRequired("cluster-id")
109+
_ = runCmd.MarkFlagRequired("investigation")
110+
_ = runCmd.MarkFlagRequired("environment")
111+
_ = runCmd.MarkFlagRequired("reason")
112+
89113
_ = runCmd.RegisterFlagCompletionFunc("investigation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
90114
return validInvestigations, cobra.ShellCompDirectiveNoFileComp
91115
})
@@ -102,6 +126,9 @@ func (o *cadRunOptions) run() error {
102126
return err
103127
}
104128

129+
grafanaURL := viper.GetString(setup.CADGrafanaURL)
130+
awsAccountID := viper.GetString(setup.CADAWSAccountID)
131+
105132
cadClusterID, cadNamespace := o.getCADClusterConfig()
106133

107134
// CAD clusters are always in production OCM, so explicitly create a production connection
@@ -123,42 +150,46 @@ func (o *cadRunOptions) run() error {
123150
return fmt.Errorf("failed to schedule task: %w", err)
124151
}
125152

126-
reportCmd := fmt.Sprintf("'osdctl cluster reports list -C %s -l 1'", o.clusterID)
127-
fmt.Println("Successfully scheduled manual investigation. It can take several minutes until a report is available. Run this command to check the latest report for the results while being connected to the right OCM backplane environment. " + reportCmd)
153+
// Get the generated name created by the API server
154+
pipelineRunName := u.GetName()
155+
156+
var logsLink string
157+
if grafanaURL != "" && awsAccountID != "" {
158+
logsLink = fmt.Sprintf("%s/explore?schemaVersion=1&panes=%%7B%%22buh%%22:%%7B%%22datasource%%22:%%22P1A97A9592CB7F392%%22,%%22queries%%22:%%5B%%7B%%22id%%22:%%22%%22,%%22region%%22:%%22us-east-1%%22,%%22namespace%%22:%%22%%22,%%22refId%%22:%%22A%%22,%%22datasource%%22:%%7B%%22type%%22:%%22cloudwatch%%22,%%22uid%%22:%%22P1A97A9592CB7F392%%22%%7D,%%22queryMode%%22:%%22Logs%%22,%%22logGroups%%22:%%5B%%7B%%22arn%%22:%%22arn:aws:logs:us-east-1:%[2]s:log-group:cads01ue1.configuration-anomaly-detection-stage:%%2A%%22,%%22name%%22:%%22cads01ue1.configuration-anomaly-detection-stage%%22,%%22accountId%%22:%%22%[2]s%%22%%7D,%%7B%%22arn%%22:%%22arn:aws:logs:us-east-1:%[2]s:log-group:cadp01ue1.configuration-anomaly-detection-production:%%2A%%22,%%22name%%22:%%22cadp01ue1.configuration-anomaly-detection-production%%22,%%22accountId%%22:%%22%[2]s%%22%%7D%%5D,%%22expression%%22:%%22fields%%20message%%5Cn%%7C%%20filter%%20kubernetes.pod_name%%20like%%20%%5C%%22%s%%5C%%22%%22,%%22statsGroups%%22:%%5B%%5D%%7D%%5D,%%22range%%22:%%7B%%22from%%22:%%22now-1h%%22,%%22to%%22:%%22now%%22%%7D,%%22panelsState%%22:%%7B%%22logs%%22:%%7B%%22visualisationType%%22:%%22logs%%22%%7D%%7D%%7D%%7D&orgId=1", grafanaURL, awsAccountID, pipelineRunName)
159+
}
160+
161+
if !o.isDryRun {
162+
reportCmd := fmt.Sprintf("'osdctl cluster reports list -C %s -l 1'", o.clusterID)
163+
msg := "Successfully scheduled manual investigation. It can take several minutes until a report is available. \n" +
164+
"Run this command to check the latest report for the results while being connected to the right OCM backplane environment. " + reportCmd + " \n"
165+
166+
if logsLink != "" {
167+
msg += "If a report fails to show up, check the TaskRun pod logs here after a few minutes: " + logsLink
168+
} else {
169+
msg += "To view TaskRun pod logs, configure 'cad_grafana_url' and 'cad_aws_account_id' using 'osdctl setup'"
170+
}
171+
fmt.Println(msg)
172+
} else {
173+
if logsLink != "" {
174+
fmt.Println("Dry-run investigation scheduled. Check for logs here: ", logsLink)
175+
} else {
176+
fmt.Println("Dry-run investigation scheduled. To view logs, configure 'cad_grafana_url' and 'cad_aws_account_id' using 'osdctl setup'")
177+
}
178+
}
128179

129180
return nil
130181
}
131182

132183
func (o *cadRunOptions) validate() error {
133-
conn, err := utils.CreateConnection()
134-
if err != nil {
135-
return err
136-
}
137-
defer conn.Close()
138-
139184
if o.clusterID == "" {
140185
return fmt.Errorf("cluster-id is required")
141186
}
142187

143-
validInvestigation := false
144-
for _, v := range validInvestigations {
145-
if o.investigation == v {
146-
validInvestigation = true
147-
break
148-
}
149-
}
150-
if !validInvestigation {
188+
if !slices.Contains(validInvestigations, o.investigation) {
151189
return fmt.Errorf("invalid investigation %q, must be one of: %v", o.investigation, validInvestigations)
152190
}
153191

154-
validEnvironment := false
155-
for _, v := range validEnvironments {
156-
if o.environment == v {
157-
validEnvironment = true
158-
break
159-
}
160-
}
161-
if !validEnvironment {
192+
if !slices.Contains(validEnvironments, o.environment) {
162193
return fmt.Errorf("invalid environment %q, must be one of: %v", o.environment, validEnvironments)
163194
}
164195

@@ -171,9 +202,9 @@ func (o *cadRunOptions) validate() error {
171202

172203
func (o *cadRunOptions) getCADClusterConfig() (clusterID, namespace string) {
173204
if o.environment == "stage" {
174-
return cadClusterIDStage, "configuration-anomaly-detection-stage"
205+
return cadClusterIDStage, cadNamespaceStage
175206
}
176-
return cadClusterIDProd, "configuration-anomaly-detection-production"
207+
return cadClusterIDProd, cadNamespaceProd
177208
}
178209

179210
func (o *cadRunOptions) pipelineRunTemplate(cadNamespace string) *unstructured.Unstructured {
@@ -197,7 +228,7 @@ func (o *cadRunOptions) pipelineRunTemplate(cadNamespace string) *unstructured.U
197228
},
198229
{
199230
"name": "dry-run",
200-
"value": "false",
231+
"value": o.isDryRun,
201232
},
202233
},
203234
"pipelineRef": map[string]interface{}{

cmd/cluster/cad/run_test.go

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cad
22

33
import (
4+
"strings"
45
"testing"
56

7+
"github.com/spf13/viper"
68
"github.com/stretchr/testify/assert"
79
)
810

@@ -56,29 +58,41 @@ func TestPipelineRunTemplate(t *testing.T) {
5658
clusterID string
5759
investigation string
5860
cadNamespace string
61+
isDryRun bool
5962
expectedNamespace string
6063
}{
6164
{
6265
name: "basic pipeline run",
6366
clusterID: "test-cluster-123",
6467
investigation: "chgm",
6568
cadNamespace: "configuration-anomaly-detection-production",
69+
isDryRun: false,
6670
expectedNamespace: "configuration-anomaly-detection-production",
6771
},
6872
{
6973
name: "stage environment pipeline run",
7074
clusterID: "stage-cluster-456",
7175
investigation: "cmbb",
7276
cadNamespace: "configuration-anomaly-detection-stage",
77+
isDryRun: false,
7378
expectedNamespace: "configuration-anomaly-detection-stage",
7479
},
80+
{
81+
name: "dry-run pipeline run",
82+
clusterID: "test-cluster-789",
83+
investigation: "ai",
84+
cadNamespace: "configuration-anomaly-detection-production",
85+
isDryRun: true,
86+
expectedNamespace: "configuration-anomaly-detection-production",
87+
},
7588
}
7689

7790
for _, tt := range tests {
7891
t.Run(tt.name, func(t *testing.T) {
7992
opts := &cadRunOptions{
8093
clusterID: tt.clusterID,
8194
investigation: tt.investigation,
95+
isDryRun: tt.isDryRun,
8296
}
8397

8498
result := opts.pipelineRunTemplate(tt.cadNamespace)
@@ -103,7 +117,118 @@ func TestPipelineRunTemplate(t *testing.T) {
103117
assert.Equal(t, tt.investigation, params[1]["value"], "investigation value should match")
104118

105119
assert.Equal(t, "dry-run", params[2]["name"], "third param should be dry-run")
106-
assert.Equal(t, "false", params[2]["value"], "dry-run should be false")
120+
assert.Equal(t, tt.isDryRun, params[2]["value"], "dry-run value should match")
107121
})
108122
}
109123
}
124+
125+
func TestLogsLinkGeneration(t *testing.T) {
126+
tests := []struct {
127+
name string
128+
grafanaURL string
129+
awsAccountID string
130+
pipelineRunName string
131+
expectLogsLink bool
132+
expectedURLContains string
133+
expectedMessage string
134+
}{
135+
{
136+
name: "both config values set",
137+
grafanaURL: "https://grafana.example.com",
138+
awsAccountID: "123456789012",
139+
pipelineRunName: "cad-manual-xyz123",
140+
expectLogsLink: true,
141+
expectedURLContains: "https://grafana.example.com/explore",
142+
expectedMessage: "",
143+
},
144+
{
145+
name: "grafana URL missing",
146+
grafanaURL: "",
147+
awsAccountID: "123456789012",
148+
pipelineRunName: "cad-manual-xyz123",
149+
expectLogsLink: false,
150+
expectedURLContains: "",
151+
expectedMessage: "To view TaskRun pod logs, configure 'cad_grafana_url' and 'cad_aws_account_id' using 'osdctl setup'",
152+
},
153+
{
154+
name: "AWS account ID missing",
155+
grafanaURL: "https://grafana.example.com",
156+
awsAccountID: "",
157+
pipelineRunName: "cad-manual-xyz123",
158+
expectLogsLink: false,
159+
expectedURLContains: "",
160+
expectedMessage: "To view TaskRun pod logs, configure 'cad_grafana_url' and 'cad_aws_account_id' using 'osdctl setup'",
161+
},
162+
{
163+
name: "both config values missing",
164+
grafanaURL: "",
165+
awsAccountID: "",
166+
pipelineRunName: "cad-manual-xyz123",
167+
expectLogsLink: false,
168+
expectedURLContains: "",
169+
expectedMessage: "To view TaskRun pod logs, configure 'cad_grafana_url' and 'cad_aws_account_id' using 'osdctl setup'",
170+
},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
// Reset viper config before each test
176+
viper.Reset()
177+
178+
// Set config values
179+
if tt.grafanaURL != "" {
180+
viper.Set("cad_grafana_url", tt.grafanaURL)
181+
}
182+
if tt.awsAccountID != "" {
183+
viper.Set("cad_aws_account_id", tt.awsAccountID)
184+
}
185+
186+
// Simulate the logs link generation logic from run.go
187+
grafanaURL := viper.GetString("cad_grafana_url")
188+
awsAccountID := viper.GetString("cad_aws_account_id")
189+
190+
if tt.expectLogsLink {
191+
assert.NotEmpty(t, grafanaURL, "grafana URL should be set")
192+
assert.NotEmpty(t, awsAccountID, "AWS account ID should be set")
193+
194+
// Verify the logs link would be generated correctly
195+
if grafanaURL != "" && awsAccountID != "" {
196+
// Simple check that the URL would be constructed
197+
assert.Contains(t, tt.expectedURLContains, grafanaURL, "grafana URL should be in the expected URL")
198+
}
199+
} else {
200+
// Verify that at least one config value is missing
201+
assert.True(t, grafanaURL == "" || awsAccountID == "", "at least one config value should be missing")
202+
}
203+
})
204+
}
205+
}
206+
207+
func TestLogsLinkURLConstruction(t *testing.T) {
208+
// Test that the logs link URL is properly constructed with all required parameters
209+
viper.Reset()
210+
viper.Set("cad_grafana_url", "https://grafana.test.com")
211+
viper.Set("cad_aws_account_id", "999888777666")
212+
213+
grafanaURL := viper.GetString("cad_grafana_url")
214+
awsAccountID := viper.GetString("cad_aws_account_id")
215+
pipelineRunName := "cad-manual-test123"
216+
217+
// Construct a simplified version of the logs link to verify format
218+
if grafanaURL != "" && awsAccountID != "" {
219+
// The actual URL is very long, so we'll just verify the key components
220+
assert.Equal(t, "https://grafana.test.com", grafanaURL)
221+
assert.Equal(t, "999888777666", awsAccountID)
222+
assert.NotEmpty(t, pipelineRunName)
223+
224+
// Verify that all account IDs would be included (there are 4 occurrences in the URL)
225+
expectedAccountIDCount := 4
226+
actualCount := strings.Count(
227+
strings.Repeat(awsAccountID+" ", expectedAccountIDCount),
228+
awsAccountID,
229+
)
230+
assert.Equal(t, expectedAccountIDCount, actualCount, "should have 4 account ID references in the URL")
231+
} else {
232+
t.Fatal("Expected config values to be set")
233+
}
234+
}

0 commit comments

Comments
 (0)