Skip to content

Commit 1cb7f12

Browse files
Merge pull request #78 from drpaneas/servicelog
OSD-6004 - Add command to template and post ServiceLogs to OCM
2 parents 9fbd9c8 + 9f369a2 commit 1cb7f12

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1592
-250
lines changed

cmd/account/cli.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func newCmdCli(streams genericclioptions.IOStreams, flags *genericclioptions.Con
2828
ops.k8sclusterresourcefactory.AttachCobraCliFlags(cliCmd)
2929

3030
cliCmd.Flags().StringVarP(&ops.output, "out", "o", "default", "Output format [default | json | env]")
31-
cliCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
31+
cliCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
3232

3333
return cliCmd
3434
}

cmd/account/console.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func newCmdConsole(streams genericclioptions.IOStreams, flags *genericclioptions
2929

3030
ops.k8sclusterresourcefactory.AttachCobraCliFlags(consoleCmd)
3131

32-
consoleCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
32+
consoleCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
3333

3434
return consoleCmd
3535
}

cmd/account/servicequotas/describe.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func newCmdDescribe(streams genericclioptions.IOStreams, flags *genericclioption
3838

3939
describeCmd.Flags().BoolVarP(&ops.allRegions, "all-regions", "", false, "Loop through all supported regions")
4040

41-
describeCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
41+
describeCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
4242

4343
return describeCmd
4444
}

cmd/account/servicequotas/update.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func newCmdUpdate(streams genericclioptions.IOStreams, flags *genericclioptions.
3838

3939
updateCmd.Flags().BoolVarP(&ops.allRegions, "all-regions", "", false, "Loop through all supported regions")
4040

41-
updateCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
41+
updateCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
4242

4343
return updateCmd
4444
}

cmd/account/verify-secrets.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func newCmdVerifySecrets(streams genericclioptions.IOStreams, flags *genericclio
4040

4141
verifySecretsCmd.Flags().StringVar(&ops.accountNamespace, "account-namespace", common.AWSAccountNamespace,
4242
"The namespace to keep AWS accounts. The default value is aws-account-operator.")
43-
verifySecretsCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
43+
verifySecretsCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
4444
verifySecretsCmd.Flags().BoolVarP(&ops.all, "all", "A", false, "Verify all Account CRs")
4545

4646
return verifySecretsCmd

cmd/federatedrole/apply.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func newCmdApply(streams genericclioptions.IOStreams, flags *genericclioptions.C
4343

4444
applyCmd.Flags().StringVarP(&ops.url, "url", "u", "", "The URL of federated role yaml file")
4545
applyCmd.Flags().StringVarP(&ops.file, "file", "f", "", "The path of federated role yaml file")
46-
applyCmd.Flags().BoolVarP(&ops.verbose, "verbose", "v", false, "Verbose output")
46+
applyCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output")
4747

4848
return applyCmd
4949
}

cmd/post.go

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
package cmd
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"github.com/openshift-online/ocm-cli/pkg/arguments"
7+
"github.com/openshift-online/ocm-cli/pkg/dump"
8+
"github.com/openshift-online/ocm-cli/pkg/ocm"
9+
sdk "github.com/openshift-online/ocm-sdk-go"
10+
"github.com/openshift/osd-utils-cli/internal/servicelog"
11+
"github.com/openshift/osd-utils-cli/internal/utils"
12+
log "github.com/sirupsen/logrus"
13+
"github.com/spf13/cobra"
14+
"io/ioutil"
15+
"net/url"
16+
"os"
17+
"path/filepath"
18+
"strings"
19+
)
20+
21+
var (
22+
template, clusterUUID, caseID string
23+
isURL bool
24+
HTMLBody []byte
25+
Message servicelog.Message
26+
GoodReply servicelog.GoodReply
27+
BadReply servicelog.BadReply
28+
)
29+
30+
const (
31+
defaultTemplate = ""
32+
defaultClusterUUID = ""
33+
defaultCaseID = ""
34+
targetAPIPath = "/api/service_logs/v1/cluster_logs" // https://api.openshift.com/?urls.primaryName=Service%20logs#/default/post_api_service_logs_v1_cluster_logs
35+
modifiedJSON = "modified-template.json"
36+
clusterParameter = "${CLUSTER_UUID}"
37+
caseIDParameter = "${CASE_ID}"
38+
clusterUUIDLongName = "cluster-external-id"
39+
caseIDLongName = "support-case-id"
40+
clusterUUIDShorthand = "c"
41+
caseIDShorthand = "i"
42+
)
43+
44+
// postCmd represents the post command
45+
var postCmd = &cobra.Command{
46+
Use: "post",
47+
Short: "Send a servicelog message to a given cluster",
48+
Run: func(cmd *cobra.Command, args []string) {
49+
readTemplate() // verify and parse
50+
replaceFlags(clusterUUID, defaultClusterUUID, clusterParameter, clusterUUIDLongName, clusterUUIDShorthand)
51+
replaceFlags(caseID, defaultCaseID, caseIDParameter, caseIDLongName, caseIDShorthand)
52+
53+
dir := tempDir()
54+
defer cleanup(dir)
55+
56+
newData := modifyTemplate(dir)
57+
58+
// Create an OCM client to talk to the cluster API
59+
// the user has to be logged in (e.g. 'ocm login')
60+
ocmClient := createConnection()
61+
defer func() {
62+
if err := ocmClient.Close(); err != nil {
63+
log.Errorf("Cannot close the ocmClient (possible memory leak): %q", err)
64+
}
65+
}()
66+
67+
// Use the OCM client to create the POST request
68+
// send it as logservice and validate the response
69+
request := createRequest(ocmClient, newData)
70+
response := postRequest(request)
71+
check(response, dir)
72+
},
73+
}
74+
75+
func init() {
76+
// define required flags
77+
postCmd.Flags().StringVarP(&template, "template", "t", defaultTemplate, "Message template file or URL")
78+
postCmd.Flags().StringVarP(&clusterUUID, clusterUUIDLongName, clusterUUIDShorthand, defaultClusterUUID, "Target cluster UUID")
79+
postCmd.Flags().StringVarP(&caseID, caseIDLongName, caseIDShorthand, defaultCaseID, "Related ticket (RedHat Support Case ID)")
80+
}
81+
82+
// accessTemplate checks if the provided template is currently accessible and returns an error
83+
func accessTemplate(template string) (err error) {
84+
85+
if template == "" {
86+
log.Errorf("Template file is not provided. Use '-t' to fix this.")
87+
return err
88+
}
89+
90+
if utils.FileExists(template) {
91+
return err
92+
}
93+
94+
if utils.FolderExists(template) {
95+
log.Errorf("the provided template %q is a directory, not a file!", template)
96+
}
97+
98+
if utils.IsValidUrl(template) {
99+
urlPage, _ := url.Parse(template)
100+
if err := utils.IsOnline(*urlPage); err != nil {
101+
log.Errorf("host %q is not accessible", template)
102+
} else {
103+
HTMLBody, err = utils.CurlThis(urlPage.String())
104+
if err == nil {
105+
isURL = true
106+
}
107+
return err
108+
}
109+
}
110+
return fmt.Errorf("cannot read the template %q", template)
111+
112+
}
113+
114+
// parseTemplate reads the template file into a JSON struct
115+
func parseTemplate(jsonFile []byte) error {
116+
return json.Unmarshal(jsonFile, &Message)
117+
}
118+
119+
func parseGoodReply(jsonFile []byte) error {
120+
return json.Unmarshal(jsonFile, &GoodReply)
121+
}
122+
123+
func parseBadReply(jsonFile []byte) error {
124+
return json.Unmarshal(jsonFile, &BadReply)
125+
}
126+
127+
func readTemplate() {
128+
if err := accessTemplate(template); err == nil { // check if this URL or file and if we can access it
129+
var file []byte
130+
if isURL {
131+
// template is URL on the web
132+
file = HTMLBody
133+
} else {
134+
// template is file on the disk
135+
file, err = ioutil.ReadFile(template) // this works only for files
136+
if err != nil {
137+
log.Fatalf("Cannot not read the file.\nError: %q\n", err)
138+
}
139+
}
140+
141+
if err = parseTemplate(file); err != nil {
142+
log.Fatalf("Cannot not parse the JSON template.\nError: %q\n", err)
143+
}
144+
} else {
145+
log.Fatal(err)
146+
}
147+
}
148+
149+
func replaceFlags(flagName, flagDefaultValue, flagParameter, flagLongName, flagShorthand string) {
150+
if err := strings.Compare(flagName, flagDefaultValue); err == 0 {
151+
// The user didn't set the flag. Check if the template is using the flag.
152+
if found := Message.SearchFlag(flagParameter); found == true {
153+
log.Fatalf("The selected template is using '%s' parameter, but '%s' flag is not set. Use '-%s' to fix this.", flagParameter, flagLongName, flagShorthand)
154+
}
155+
} else {
156+
// The user set the flag. Check if the template is using the flag.
157+
if found := Message.SearchFlag(flagParameter); found == false {
158+
log.Fatalf("The selected template is not using '%s' parameter, but '%s' flag is set. Do not use '-%s' to fix this.", flagParameter, flagLongName, flagShorthand)
159+
}
160+
Message.ReplaceWithFlag(flagParameter, flagName)
161+
}
162+
}
163+
164+
func tempDir() (dir string) {
165+
if dirPath, err := os.Getwd(); err != nil {
166+
log.Error(err)
167+
} else {
168+
dir, err = ioutil.TempDir(dirPath, "servicelog-")
169+
if err != nil {
170+
log.Fatal(err)
171+
}
172+
}
173+
return dir
174+
}
175+
176+
func modifyTemplate(dir string) (newData string) {
177+
// Write the modified file
178+
newData = filepath.Join(dir, modifiedJSON)
179+
if err := utils.CreateFile(newData); err == nil {
180+
file, err := os.Create(newData)
181+
if err != nil {
182+
log.Fatalf("Cannot overwrite file %q", err)
183+
}
184+
defer file.Close()
185+
186+
// Create the corrected JSON
187+
s, _ := json.MarshalIndent(Message, "", "\t")
188+
if _, err := file.WriteString(string(s)); err != nil {
189+
log.Fatalf("Cannot write the new modified template %q", err)
190+
}
191+
} else {
192+
log.Fatalf("Cannot create file %q", err)
193+
}
194+
return newData
195+
}
196+
197+
func createConnection() *sdk.Connection {
198+
connection, err := ocm.NewConnection().Build()
199+
if err != nil {
200+
if strings.Contains(err.Error(), "Not logged in, run the") {
201+
log.Fatalf("Failed to create OCM connection: Authetication error, run the 'ocm login' command first.")
202+
}
203+
log.Fatalf("Failed to create OCM connection: %v", err)
204+
}
205+
return connection
206+
}
207+
208+
func createRequest(ocmClient *sdk.Connection, newData string) *sdk.Request {
209+
// Create and populate the request:
210+
request := ocmClient.Post()
211+
err := arguments.ApplyPathArg(request, targetAPIPath)
212+
if err != nil {
213+
log.Fatalf("Can't parse API path '%s': %v\n", targetAPIPath, err)
214+
}
215+
var empty []string
216+
arguments.ApplyParameterFlag(request, empty)
217+
arguments.ApplyHeaderFlag(request, empty)
218+
err = arguments.ApplyBodyFlag(request, newData)
219+
if err != nil {
220+
log.Fatalf("Can't read body: %v", err)
221+
}
222+
return request
223+
}
224+
225+
func postRequest(request *sdk.Request) *sdk.Response {
226+
response, err := request.Send()
227+
if err != nil {
228+
log.Fatalf("Can't send request: %v", err)
229+
}
230+
return response
231+
}
232+
233+
func check(response *sdk.Response, dir string) {
234+
status := response.Status()
235+
236+
body := response.Bytes()
237+
238+
if status < 400 {
239+
validateGoodResponse(body)
240+
log.Info("Message has been successfully sent")
241+
242+
} else {
243+
validateBadResponse(body)
244+
cleanup(dir)
245+
log.Fatalf("Failed to post message because of %q", BadReply.Reason)
246+
247+
}
248+
}
249+
250+
func validateGoodResponse(body []byte) {
251+
if err := parseGoodReply(body); err != nil {
252+
log.Fatalf("Cannot not parse the JSON template.\nError: %q\n", err)
253+
}
254+
255+
severity := GoodReply.Severity
256+
if severity != Message.Severity {
257+
log.Fatalf("Message sent, but wrong severity information was passed (wanted %q, got %q)", Message.Severity, severity)
258+
}
259+
serviceName := GoodReply.ServiceName
260+
if serviceName != Message.ServiceName {
261+
log.Fatalf("Message sent, but wrong service_name information was passed (wanted %q, got %q)", Message.ServiceName, serviceName)
262+
}
263+
clusteruuid := GoodReply.ClusterUUID
264+
if clusterUUID != clusteruuid {
265+
log.Fatalf("Message sent, but to different cluster (wanted %q, got %q)", clusterUUID, clusteruuid)
266+
}
267+
summary := GoodReply.Summary
268+
if summary != Message.Summary {
269+
log.Fatalf("Message sent, but wrong summary information was passed (wanted %q, got %q)", Message.Summary, summary)
270+
}
271+
description := GoodReply.Description
272+
if description != Message.Description {
273+
log.Fatalf("Message sent, but wrong description information was passed (wanted %q, got %q)", Message.Description, description)
274+
}
275+
276+
if err := dump.Pretty(os.Stdout, body); err != nil {
277+
log.Fatalf("Server returned invalid JSON reply %q", err)
278+
}
279+
}
280+
281+
func validateBadResponse(body []byte) {
282+
if err := dump.Pretty(os.Stderr, body); err != nil {
283+
log.Errorf("Server returned invalid JSON reply %q", err)
284+
}
285+
286+
if err := parseBadReply(body); err != nil {
287+
log.Fatalf("Cannot parse the error JSON message %q", err)
288+
}
289+
}
290+
291+
func cleanup(dir string) {
292+
if err := os.RemoveAll(dir); err != nil {
293+
log.Errorf("Cannot clean up %q", err)
294+
}
295+
}

cmd/root.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ package cmd
22

33
import (
44
"flag"
5-
"os"
6-
75
routev1 "github.com/openshift/api/route/v1"
86
awsv1alpha1 "github.com/openshift/aws-account-operator/pkg/apis/aws/v1alpha1"
97
hivev1 "github.com/openshift/hive/pkg/apis/hive/v1"
108
"github.com/spf13/cobra"
9+
"os"
1110

1211
"k8s.io/cli-runtime/pkg/genericclioptions"
1312
"k8s.io/client-go/kubernetes/scheme"
@@ -28,7 +27,6 @@ func init() {
2827
_ = awsv1alpha1.AddToScheme(scheme.Scheme)
2928
_ = routev1.AddToScheme(scheme.Scheme)
3029
_ = hivev1.AddToScheme(scheme.Scheme)
31-
3230
NewCmdRoot(genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
3331
}
3432

@@ -63,6 +61,7 @@ func NewCmdRoot(streams genericclioptions.IOStreams) *cobra.Command {
6361
rootCmd.AddCommand(federatedrole.NewCmdFederatedRole(streams, kubeFlags))
6462
rootCmd.AddCommand(network.NewCmdNetwork(streams, kubeFlags))
6563
rootCmd.AddCommand(newCmdMetrics(streams, kubeFlags))
64+
rootCmd.AddCommand(servicelogCmd)
6665

6766
// add docs command
6867
rootCmd.AddCommand(newCmdDocs(streams))

cmd/servicelog.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cmd
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
// servicelogCmd represents the servicelog command
8+
var servicelogCmd = &cobra.Command{
9+
Use: "servicelog",
10+
Short: "OCM/Hive Service log",
11+
Run: func(cmd *cobra.Command, args []string) {
12+
cmd.Help()
13+
},
14+
}
15+
16+
func init() {
17+
// Add subcommands
18+
servicelogCmd.AddCommand(postCmd) // servicelog post
19+
}

0 commit comments

Comments
 (0)