Skip to content

Commit 31022a3

Browse files
committed
feat: send servicelog
1 parent 4c2023c commit 31022a3

File tree

15 files changed

+1184
-5
lines changed

15 files changed

+1184
-5
lines changed

cmd/post.go

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

cmd/servicelog.go

+5
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,8 @@ var servicelogCmd = &cobra.Command{
1212
cmd.Help()
1313
},
1414
}
15+
16+
func init() {
17+
// Add subcommands
18+
servicelogCmd.AddCommand(postCmd) // servicelog post
19+
}

go.mod

+6-5
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ require (
88
github.com/fsnotify/fsnotify v1.4.9 // indirect
99
github.com/golang/mock v1.4.4
1010
github.com/onsi/gomega v1.10.1
11+
github.com/openshift-online/ocm-cli v0.1.47
12+
github.com/openshift-online/ocm-sdk-go v0.1.152
1113
github.com/openshift/api v3.9.1-0.20191111211345-a27ff30ebf09+incompatible
1214
github.com/openshift/aws-account-operator v0.0.0-20200914143350-bbda1c91242b
1315
github.com/openshift/hive v1.0.5
1416
github.com/pkg/errors v0.9.1
15-
github.com/prometheus/common v0.10.0
17+
github.com/prometheus/common v0.11.1
18+
github.com/sirupsen/logrus v1.6.0
1619
github.com/spf13/cobra v1.0.0
1720
github.com/spf13/pflag v1.0.5
18-
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
19-
golang.org/x/text v0.3.3 // indirect
20-
k8s.io/api v0.18.3
21-
k8s.io/apimachinery v0.18.3
21+
k8s.io/api v0.18.5
22+
k8s.io/apimachinery v0.18.5
2223
k8s.io/cli-runtime v0.18.3
2324
k8s.io/client-go v12.0.0+incompatible
2425
k8s.io/klog v1.0.0

0 commit comments

Comments
 (0)