Skip to content

Commit 0562607

Browse files
committed
Merge remote-tracking branch 'origin/main' into upload-file
2 parents fbad2aa + f4a900a commit 0562607

File tree

5 files changed

+320
-35
lines changed

5 files changed

+320
-35
lines changed

.github/workflows/build-pr-cmk.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
name: Build cmk binaries on PR
19+
20+
on:
21+
pull_request_target:
22+
types: [opened, synchronize, reopened]
23+
24+
concurrency:
25+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
26+
cancel-in-progress: true
27+
28+
jobs:
29+
build:
30+
permissions:
31+
contents: read
32+
runs-on: ubuntu-24.04
33+
env:
34+
GITHUB_TOKEN: ""
35+
outputs:
36+
outcome: ${{ steps.meta.outputs.outcome }}
37+
artifact_url: ${{ steps.meta.outputs.artifact_url }}
38+
steps:
39+
- name: Checkout PR HEAD
40+
uses: actions/checkout@v4
41+
with:
42+
ref: ${{ github.event.pull_request.head.sha }}
43+
persist-credentials: false
44+
45+
- name: Set up Go
46+
uses: actions/setup-go@v5
47+
with:
48+
go-version: '1.22'
49+
50+
- name: Build dist
51+
id: build
52+
run: make dist
53+
continue-on-error: true
54+
55+
- name: Upload zipped dist artifact
56+
id: upload_artifact
57+
if: ${{ steps.build.outcome == 'success' }} # gate on build outcome
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: cmk-binaries.pr${{ github.event.pull_request.number }}
61+
path: dist/
62+
if-no-files-found: error
63+
retention-days: 10
64+
65+
- name: Expose build outcome & artifact link
66+
id: meta
67+
if: always()
68+
run: |
69+
echo "outcome=${{ steps.build.outcome }}" >> $GITHUB_OUTPUT
70+
echo "artifact_url=${{ steps.upload_artifact.outputs.artifact-url }}" >> $GITHUB_OUTPUT
71+
72+
comment:
73+
if: always()
74+
needs: build
75+
permissions:
76+
contents: read
77+
issues: write
78+
pull-requests: write
79+
runs-on: ubuntu-24.04
80+
steps:
81+
- name: Comment or update cmk build artifact on PR
82+
uses: actions/github-script@v7
83+
with:
84+
script: |
85+
const { execSync } = require('child_process');
86+
87+
const issue_number = context.payload.pull_request.number;
88+
const identifier = "cmk-build-artifact-comment";
89+
90+
const owner = context.payload.repository.owner.login; // base repo (pull_request_target)
91+
const repo = context.payload.repository.name;
92+
93+
const buildOutcome = "${{ needs.build.outputs.outcome }}";
94+
const artifactUrl = "${{ needs.build.outputs.artifact_url }}";
95+
const runId = "${{ github.run_id }}";
96+
97+
core.info(`Will comment on ${owner}/${repo}#${issue_number}`);
98+
core.info(`Outcome=${buildOutcome || '(empty)'} Artifact=${artifactUrl || '(none)'}`);
99+
100+
let body = `<!-- ${identifier} -->\n`;
101+
if (buildOutcome === 'success' && artifactUrl) {
102+
const expiryDate = execSync("date -d '+10 days' '+%B %d, %Y'").toString().trim();
103+
body += `✅ Build complete for PR #${issue_number}.\n\n`;
104+
body += `🔗 Download the [cmk binaries](${artifactUrl}) (expires on ${expiryDate})`;
105+
} else {
106+
body += `❌ Build failed for PR #${issue_number}.\n\n`;
107+
body += `See the run: https://github.com/${owner}/${repo}/actions/runs/${runId}`;
108+
}
109+
110+
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number });
111+
const existing = comments.find(c => c.user.login === 'github-actions[bot]' && c.body.includes(identifier));
112+
113+
if (existing) {
114+
core.info(`Updating comment id ${existing.id}`);
115+
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
116+
} else {
117+
core.info(`Creating new comment`);
118+
await github.rest.issues.createComment({ owner, repo, issue_number, body });
119+
}
120+

cmd/network.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,84 @@ func findSessionCookie(cookies []*http.Cookie) *http.Cookie {
4949
return nil
5050
}
5151

52+
func getLoginResponse(responseBody []byte) (map[string]interface{}, error) {
53+
var responseMap map[string]interface{}
54+
err := json.Unmarshal(responseBody, &responseMap)
55+
if err != nil {
56+
return nil, errors.New("failed to parse login response: " + err.Error())
57+
}
58+
loginRespRaw, ok := responseMap["loginresponse"]
59+
if !ok {
60+
return nil, errors.New("failed to parse login response, expected 'loginresponse' key not found")
61+
}
62+
loginResponse, ok := loginRespRaw.(map[string]interface{})
63+
if !ok {
64+
return nil, errors.New("failed to parse login response, expected 'loginresponse' to be a map")
65+
}
66+
return loginResponse, nil
67+
}
68+
69+
func getResponseBooleanValue(response map[string]interface{}, key string) (bool, bool) {
70+
v, found := response[key]
71+
if !found {
72+
return false, false
73+
}
74+
switch value := v.(type) {
75+
case bool:
76+
return true, value
77+
case string:
78+
return true, strings.ToLower(value) == "true"
79+
case float64:
80+
return true, value != 0
81+
default:
82+
return true, false
83+
}
84+
}
85+
86+
func checkLogin2FAPromptAndValidate(r *Request, response map[string]interface{}, sessionKey string) error {
87+
if !r.Config.HasShell {
88+
return nil
89+
}
90+
config.Debug("Checking if 2FA is enabled and verified for the user ", response)
91+
found, is2faEnabled := getResponseBooleanValue(response, "is2faenabled")
92+
if !found || !is2faEnabled {
93+
config.Debug("2FA is not enabled for the user, skipping 2FA validation")
94+
return nil
95+
}
96+
found, is2faVerified := getResponseBooleanValue(response, "is2faverified")
97+
if !found || is2faVerified {
98+
config.Debug("2FA is already verified for the user, skipping 2FA validation")
99+
return nil
100+
}
101+
activeSpinners := r.Config.PauseActiveSpinners()
102+
fmt.Print("Enter 2FA code: ")
103+
var code string
104+
fmt.Scanln(&code)
105+
if activeSpinners > 0 {
106+
r.Config.ResumePausedSpinners()
107+
}
108+
params := make(url.Values)
109+
params.Add("command", "validateUserTwoFactorAuthenticationCode")
110+
params.Add("codefor2fa", code)
111+
params.Add("sessionkey", sessionKey)
112+
113+
msURL, _ := url.Parse(r.Config.ActiveProfile.URL)
114+
115+
config.Debug("Validating 2FA with POST URL:", msURL, params)
116+
spinner := r.Config.StartSpinner("trying to validate 2FA...")
117+
resp, err := r.Client().PostForm(msURL.String(), params)
118+
r.Config.StopSpinner(spinner)
119+
if err != nil {
120+
return errors.New("failed to failed to validate 2FA code: " + err.Error())
121+
}
122+
config.Debug("ValidateUserTwoFactorAuthenticationCode POST response status code:", resp.StatusCode)
123+
if resp.StatusCode != http.StatusOK {
124+
r.Client().Jar, _ = cookiejar.New(nil)
125+
return errors.New("failed to validate 2FA code, please check the code. Invalidating session")
126+
}
127+
return nil
128+
}
129+
52130
// Login logs in a user based on provided request and returns http client and session key
53131
func Login(r *Request) (string, error) {
54132
params := make(url.Values)
@@ -81,6 +159,13 @@ func Login(r *Request) (string, error) {
81159
return "", e
82160
}
83161

162+
body, _ := ioutil.ReadAll(resp.Body)
163+
config.Debug("Login response body:", string(body))
164+
loginResponse, err := getLoginResponse(body)
165+
if err != nil {
166+
return "", err
167+
}
168+
84169
var sessionKey string
85170
curTime := time.Now()
86171
expiryDuration := 15 * time.Minute
@@ -98,6 +183,9 @@ func Login(r *Request) (string, error) {
98183
}()
99184

100185
config.Debug("Login sessionkey:", sessionKey)
186+
if err := checkLogin2FAPromptAndValidate(r, loginResponse, sessionKey); err != nil {
187+
return "", err
188+
}
101189
return sessionKey, nil
102190
}
103191

@@ -157,7 +245,16 @@ func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) {
157245
return nil, errors.New("async API job query timed out")
158246

159247
case <-ticker.C:
160-
queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", []string{"jobid=" + jobID}, false)
248+
args := []string{"jobid=" + jobID}
249+
if r.Args != nil {
250+
for _, arg := range r.Args {
251+
if strings.HasPrefix(strings.ToLower(arg), "filter=") {
252+
args = append(args, arg)
253+
break
254+
}
255+
}
256+
}
257+
queryResult, queryError := NewAPIRequest(r, "queryAsyncJobResult", args, false)
161258
if queryError != nil {
162259
return queryResult, queryError
163260
}

cmd/output.go

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,28 +62,49 @@ func printJSON(response map[string]interface{}) {
6262
enc.Encode(response)
6363
}
6464

65+
func getItemsFromValue(v interface{}) ([]interface{}, reflect.Kind, bool) {
66+
valueKind := reflect.TypeOf(v).Kind()
67+
if valueKind == reflect.Slice {
68+
sliceItems, ok := v.([]interface{})
69+
if !ok {
70+
return nil, valueKind, false
71+
}
72+
return sliceItems, valueKind, true
73+
} else if valueKind == reflect.Map {
74+
mapItem, ok := v.(map[string]interface{})
75+
if !ok {
76+
return nil, valueKind, false
77+
}
78+
return []interface{}{mapItem}, valueKind, true
79+
}
80+
return nil, valueKind, false
81+
}
82+
6583
func printText(response map[string]interface{}) {
6684
format := "text"
6785
for k, v := range response {
6886
valueType := reflect.TypeOf(v)
69-
if valueType.Kind() == reflect.Slice {
70-
fmt.Printf("%v:\n", k)
71-
for idx, item := range v.([]interface{}) {
72-
if idx > 0 {
73-
fmt.Println("================================================================================")
74-
}
75-
row, isMap := item.(map[string]interface{})
76-
if isMap {
77-
for field, value := range row {
78-
fmt.Printf("%s = %v\n", field, jsonify(value, format))
87+
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
88+
items, _, ok := getItemsFromValue(v)
89+
if ok {
90+
fmt.Printf("%v:\n", k)
91+
for idx, item := range items {
92+
if idx > 0 {
93+
fmt.Println("================================================================================")
94+
}
95+
row, isMap := item.(map[string]interface{})
96+
if isMap {
97+
for field, value := range row {
98+
fmt.Printf("%s = %v\n", field, jsonify(value, format))
99+
}
100+
} else {
101+
fmt.Printf("%v\n", item)
79102
}
80-
} else {
81-
fmt.Printf("%v\n", item)
82103
}
104+
return
83105
}
84-
} else {
85-
fmt.Printf("%v = %v\n", k, jsonify(v, format))
86106
}
107+
fmt.Printf("%v = %v\n", k, jsonify(v, format))
87108
}
88109
}
89110

@@ -92,8 +113,8 @@ func printTable(response map[string]interface{}, filter []string) {
92113
table := tablewriter.NewWriter(os.Stdout)
93114
for k, v := range response {
94115
valueType := reflect.TypeOf(v)
95-
if valueType.Kind() == reflect.Slice {
96-
items, ok := v.([]interface{})
116+
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
117+
items, _, ok := getItemsFromValue(v)
97118
if !ok {
98119
continue
99120
}
@@ -134,7 +155,7 @@ func printColumn(response map[string]interface{}, filter []string) {
134155
for _, v := range response {
135156
valueType := reflect.TypeOf(v)
136157
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
137-
items, ok := v.([]interface{})
158+
items, _, ok := getItemsFromValue(v)
138159
if !ok {
139160
continue
140161
}
@@ -173,7 +194,7 @@ func printCsv(response map[string]interface{}, filter []string) {
173194
for _, v := range response {
174195
valueType := reflect.TypeOf(v)
175196
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
176-
items, ok := v.([]interface{})
197+
items, _, ok := getItemsFromValue(v)
177198
if !ok {
178199
continue
179200
}
@@ -207,7 +228,7 @@ func printCsv(response map[string]interface{}, filter []string) {
207228
}
208229

209230
func filterResponse(response map[string]interface{}, filter []string, excludeFilter []string, outputType string) map[string]interface{} {
210-
if (filter == nil || len(filter) == 0) && (excludeFilter == nil || len(excludeFilter) == 0) {
231+
if len(filter) == 0 && len(excludeFilter) == 0 {
211232
return response
212233
}
213234

@@ -224,8 +245,12 @@ func filterResponse(response map[string]interface{}, filter []string, excludeFil
224245
filteredResponse := make(map[string]interface{})
225246

226247
for key, value := range response {
227-
switch items := value.(type) {
228-
case []interface{}:
248+
valueType := reflect.TypeOf(value)
249+
if valueType.Kind() == reflect.Slice || valueType.Kind() == reflect.Map {
250+
items, originalKind, ok := getItemsFromValue(value)
251+
if !ok {
252+
continue
253+
}
229254
var filteredRows []interface{}
230255
for _, item := range items {
231256
row, ok := item.(map[string]interface{})
@@ -255,9 +280,12 @@ func filterResponse(response map[string]interface{}, filter []string, excludeFil
255280

256281
filteredRows = append(filteredRows, filteredRow)
257282
}
258-
filteredResponse[key] = filteredRows
259-
260-
default:
283+
if originalKind == reflect.Map && len(filteredRows) > 0 {
284+
filteredResponse[key] = filteredRows[0]
285+
} else {
286+
filteredResponse[key] = filteredRows
287+
}
288+
} else {
261289
filteredResponse[key] = value
262290
}
263291
}

0 commit comments

Comments
 (0)