Skip to content

Commit 9038472

Browse files
authored
INS-1454: Add kyverno policies commands to insights-cli (#282)
* INS-1454: Add kyverno policies commands to insights-cli * COde clanup * COde clanup * FIx * FIx * Fix * Fix Kyverno bulk upsert request schema mismatch The API expects KyvernoPolicyInput format but CLI was sending KyvernoPolicy format. Key differences: - Field names: apiVersion (camelCase) vs api_version (snake_case) - Field types: map[string]string vs map[string]interface{} for labels/annotations - Missing fields: API doesn't expect id, organization_id, created_at, updated_at Added: - KyvernoPolicyInput struct matching API schema - ToKyvernoPolicyInput() conversion method - Updated BulkUpsertKyvernoPolicies to use conversion This fixes the 400 Bad Request error: 'Value is not nullable - doesn't match schema' * Fix * Fix * Fix Kyverno list endpoint response parsing The API returns KyvernoPolicyList structure with policies array and total count, but CLI was trying to unmarshal directly into []KyvernoPolicy. Added: - KyvernoPolicyList struct matching API response format - Updated FetchKyvernoPolicies to unmarshal into KyvernoPolicyList first - Extract policies array from the response structure This fixes the JSON unmarshaling error: 'cannot unmarshal object into Go value of type []kyverno.KyvernoPolicy' * Update test expectation to include Kyverno policies section The push_all_and_list test was failing because the actual output now includes the kyverno-policies section (which is correct behavior), but the expected output didn't include it. Updated desired_output.txt to include: kyverno-policies This reflects the new Kyverno functionality that was added to the push all and list all commands. * Empty * Empty * Fix Kyverno validation request structure The API expects ValidationRequest with 'policy' field, but CLI was sending 'policy_yaml'. Also, the API expects 'resources' as string array, not 'test_resources' as TestResource array. Added: - ValidationRequest struct matching API schema - Updated ValidateKyvernoPolicyWithExpectedOutcomes to use correct structure - Convert test resources to string array format This fixes the 400 Bad Request error: 'property "policy" is missing - doesn't match schema #/components/schemas/ValidationRequest' * Fix validate_kyverno_policies test for both local and CI environments The test now handles both scenarios: 1. Local dev environment (no CI vars): Expects 'FAIRWINDS_TOKEN must be set' 2. CI environment (with CI vars): Expects 'Organization not found' (404) This makes the test robust for both development and CI environments. The test validates that: - Token validation works correctly in local dev - API integration works correctly in CI (connects to real API) - Both scenarios properly fail as expected Updated regex pattern to match either error message: 'FAIRWINDS_TOKEN must be set|Organization not found' * Fix * Fix * Fix * Fix * Fix * Fixes * Fixes * Fix * Test * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * FIx * FIx * Fix * Fix * Fix * Fix * Fix * Fixes * Fixes * Fixes * Fixes * Code cleanup * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix * Fix
1 parent 89c4668 commit 9038472

26 files changed

Lines changed: 2206 additions & 2 deletions
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2025 FairwindsOps Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cli
16+
17+
import (
18+
"os"
19+
20+
"github.com/fairwindsops/insights-cli/pkg/kyverno"
21+
"github.com/sirupsen/logrus"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
var downloadKyvernoPoliciesSubDir string
26+
27+
func init() {
28+
downloadKyvernoPoliciesCmd.PersistentFlags().StringVar(&downloadKyvernoPoliciesSubDir, "download-subdirectory", "kyverno-policies", "Sub-directory within download-directory, to download Kyverno policies.")
29+
downloadCmd.AddCommand(downloadKyvernoPoliciesCmd)
30+
}
31+
32+
var downloadKyvernoPoliciesCmd = &cobra.Command{
33+
Use: "kyverno-policies",
34+
Short: "Download Kyverno policies from Insights to local files.",
35+
Long: "Download Kyverno policies from Insights to local files. This creates/updates the local kyverno-policies directory structure for synchronization.",
36+
Example: `
37+
# Download all policies from Insights
38+
insights-cli download kyverno-policies -d .
39+
40+
# Download to custom subdirectory
41+
insights-cli download kyverno-policies -d . --download-subdirectory my-policies
42+
43+
# Download to specific directory
44+
insights-cli download kyverno-policies -d /path/to/my/project
45+
46+
# Download to specific directory with custom subdirectory
47+
insights-cli download kyverno-policies -d /path/to/my/project --download-subdirectory policies
48+
49+
# Download with override
50+
insights-cli download kyverno-policies -d . --override`,
51+
PreRun: validateAndLoadInsightsAPIConfigWrapper,
52+
Run: func(cmd *cobra.Command, args []string) {
53+
org := configurationObject.Options.Organization
54+
kyvernoPolicies, err := kyverno.FetchKyvernoPolicies(client, org)
55+
if err != nil {
56+
logrus.Fatalf("unable to fetch kyverno-policies from insights: %v", err)
57+
}
58+
59+
// Build the full save directory path
60+
saveDir := downloadDir + "/" + downloadKyvernoPoliciesSubDir
61+
62+
// Ensure the save directory exists
63+
err = os.MkdirAll(saveDir, 0755)
64+
if err != nil {
65+
logrus.Fatalf("unable to create directory %s: %v", saveDir, err)
66+
}
67+
68+
c, err := saveEntitiesLocally(saveDir, kyvernoPolicies, overrideLocalFiles)
69+
if err != nil {
70+
logrus.Fatalf("error saving kyverno-policies locally: %v", err)
71+
}
72+
73+
logrus.Infof("Downloaded %d kyverno-policies from Insights to %s\n", c, saveDir)
74+
logrus.Infof("You can now add test cases and push changes back to Insights\n")
75+
},
76+
}

pkg/cli/list_all.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/xlab/treeprint"
2323

2424
"github.com/fairwindsops/insights-cli/pkg/appgroups"
25+
"github.com/fairwindsops/insights-cli/pkg/kyverno"
2526
"github.com/fairwindsops/insights-cli/pkg/opa"
2627
"github.com/fairwindsops/insights-cli/pkg/policymappings"
2728
"github.com/fairwindsops/insights-cli/pkg/rules"
@@ -65,6 +66,11 @@ var listAllCmd = &cobra.Command{
6566
logrus.Fatalf("error building policy-mappings tree: %v", err)
6667
}
6768

69+
err = kyverno.AddKyvernoPoliciesBranch(client, org, tree)
70+
if err != nil {
71+
logrus.Fatalf("Unable to get Kyverno policies from insights: %v", err)
72+
}
73+
6874
fmt.Println(tree.String())
6975
},
7076
}

pkg/cli/list_kyverno_policies.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Copyright 2025 FairwindsOps Inc
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cli
16+
17+
import (
18+
"fmt"
19+
"os"
20+
"path/filepath"
21+
"strings"
22+
23+
"github.com/sirupsen/logrus"
24+
"github.com/spf13/cobra"
25+
"github.com/xlab/treeprint"
26+
27+
"github.com/fairwindsops/insights-cli/pkg/kyverno"
28+
)
29+
30+
var listLocal bool
31+
var listClusterName string
32+
var listFormat string
33+
34+
func init() {
35+
listKyvernoPoliciesCmd.Flags().BoolVar(&listLocal, "local", false, "List local policy files")
36+
listKyvernoPoliciesCmd.Flags().StringVar(&listClusterName, "cluster", "", "List policies for specific cluster from Insights")
37+
listKyvernoPoliciesCmd.Flags().StringVar(&listFormat, "format", "tree", "Output format: tree, yaml")
38+
listCmd.AddCommand(listKyvernoPoliciesCmd)
39+
}
40+
41+
var listKyvernoPoliciesCmd = &cobra.Command{
42+
Use: "kyverno-policies",
43+
Short: "List Kyverno policies.",
44+
Long: "List Kyverno policies from local files or Insights. Use --local for local files, --cluster for cluster-specific policies.",
45+
Example: `
46+
# List all policies from Insights
47+
insights-cli list kyverno-policies
48+
49+
# List local policy files
50+
insights-cli list kyverno-policies --local
51+
52+
# List policies for specific cluster
53+
insights-cli list kyverno-policies --cluster production
54+
55+
# Export cluster policies as YAML
56+
insights-cli list kyverno-policies --cluster production --format yaml`,
57+
PreRun: func(cmd *cobra.Command, args []string) {
58+
// Only require API config if not listing local files
59+
if !listLocal {
60+
validateAndLoadInsightsAPIConfigWrapper(cmd, args)
61+
}
62+
},
63+
Run: func(cmd *cobra.Command, args []string) {
64+
if listLocal {
65+
// Local file system listing
66+
if _, err := os.Stat("kyverno-policies"); os.IsNotExist(err) {
67+
logrus.Fatalf("Directory kyverno-policies does not exist")
68+
}
69+
tree := treeprint.New()
70+
err := addLocalKyvernoPoliciesBranch("kyverno-policies", tree)
71+
if err != nil {
72+
logrus.Fatalf("Unable to list local policies: %v", err)
73+
}
74+
fmt.Println(tree.String())
75+
} else {
76+
// API listing
77+
org := configurationObject.Options.Organization
78+
79+
if listClusterName != "" {
80+
// Cluster-specific listing
81+
if listFormat == "yaml" {
82+
// Export as YAML
83+
yamlContent, err := kyverno.ExportClusterKyvernoPoliciesYaml(client, org, listClusterName)
84+
if err != nil {
85+
logrus.Fatalf("Unable to export cluster policies: %v", err)
86+
}
87+
fmt.Print(yamlContent)
88+
} else {
89+
// List as tree (with app groups applied by default)
90+
tree := treeprint.New()
91+
err := kyverno.AddClusterKyvernoPoliciesWithAppGroupsBranch(client, org, listClusterName, tree)
92+
if err != nil {
93+
logrus.Fatalf("Unable to get cluster policies: %v", err)
94+
}
95+
fmt.Println(tree.String())
96+
}
97+
} else {
98+
// General API listing (existing functionality)
99+
tree := treeprint.New()
100+
err := kyverno.AddKyvernoPoliciesBranch(client, org, tree)
101+
if err != nil {
102+
logrus.Fatalf("Unable to get Kyverno policies from insights: %v", err)
103+
}
104+
fmt.Println(tree.String())
105+
}
106+
}
107+
},
108+
}
109+
110+
// addLocalKyvernoPoliciesBranch builds a tree for local Kyverno policy files
111+
func addLocalKyvernoPoliciesBranch(dir string, tree treeprint.Tree) error {
112+
policiesBranch := tree.AddBranch("kyverno-policies (local)")
113+
114+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
115+
if err != nil {
116+
return err
117+
}
118+
119+
if info.IsDir() {
120+
return nil
121+
}
122+
123+
filename := filepath.Base(path)
124+
125+
// Only process policy files, exclude test case files
126+
if isPolicyFile(filename) && !isTestCaseFile(filename) {
127+
policyName := extractPolicyNameFromFile(filename)
128+
policyNode := policiesBranch.AddBranch(policyName)
129+
policyNode.AddNode(fmt.Sprintf("File: %s", filename))
130+
131+
// Try to read and display basic policy info
132+
policy, err := kyverno.ReadPolicyFromFile(path)
133+
if err == nil {
134+
if policy.Kind != "" {
135+
policyNode.AddNode(fmt.Sprintf("Kind: %s", policy.Kind))
136+
}
137+
if policy.APIVersion != "" {
138+
policyNode.AddNode(fmt.Sprintf("API Version: %s", policy.APIVersion))
139+
}
140+
}
141+
} else if isTestCaseFile(filename) {
142+
// Add test case files as nodes under their policy
143+
policyName := extractPolicyNameFromTestCase(filename)
144+
testCaseName := extractTestCaseName(filename)
145+
expectedOutcome := determineExpectedOutcome(filename)
146+
147+
// Find or create the policy node
148+
// For simplicity, always create a new policy node for test cases
149+
// In a more sophisticated implementation, we'd track existing nodes
150+
policyNode := policiesBranch.AddBranch(policyName)
151+
152+
testNode := policyNode.AddBranch("test-cases")
153+
testCaseNode := testNode.AddBranch(testCaseName)
154+
testCaseNode.AddNode(fmt.Sprintf("File: %s", filename))
155+
testCaseNode.AddNode(fmt.Sprintf("Expected: %s", expectedOutcome))
156+
}
157+
158+
return nil
159+
})
160+
161+
return err
162+
}
163+
164+
// Helper functions for local policy file processing
165+
func isPolicyFile(filename string) bool {
166+
return (strings.HasSuffix(filename, ".yaml") || strings.HasSuffix(filename, ".yml")) &&
167+
!strings.Contains(filename, ".testcase")
168+
}
169+
170+
func isTestCaseFile(filename string) bool {
171+
return strings.Contains(filename, ".testcase")
172+
}
173+
174+
func extractPolicyNameFromFile(filename string) string {
175+
name := strings.TrimSuffix(filename, ".yaml")
176+
name = strings.TrimSuffix(name, ".yml")
177+
return name
178+
}
179+
180+
func extractPolicyNameFromTestCase(filename string) string {
181+
parts := strings.Split(filename, ".")
182+
if len(parts) >= 2 {
183+
return parts[0]
184+
}
185+
return ""
186+
}
187+
188+
func extractTestCaseName(filename string) string {
189+
parts := strings.Split(filename, ".")
190+
for _, part := range parts {
191+
if strings.HasPrefix(part, "testcase") {
192+
return part
193+
}
194+
}
195+
return ""
196+
}
197+
198+
func determineExpectedOutcome(filename string) string {
199+
if strings.Contains(filename, ".success.") {
200+
return "success"
201+
}
202+
if strings.Contains(filename, ".failure.") {
203+
return "failure"
204+
}
205+
return "unknown"
206+
}

pkg/cli/push_all.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/spf13/cobra"
2323

2424
"github.com/fairwindsops/insights-cli/pkg/appgroups"
25+
"github.com/fairwindsops/insights-cli/pkg/kyverno"
2526
"github.com/fairwindsops/insights-cli/pkg/opa"
2627
"github.com/fairwindsops/insights-cli/pkg/policies"
2728
"github.com/fairwindsops/insights-cli/pkg/policymappings"
@@ -35,6 +36,7 @@ func init() {
3536
pushAllCmd.PersistentFlags().StringVarP(&pushRulesSubDir, "push-rules-subdirectory", "", defaultPushRulesSubDir, "Sub-directory within push-directory, to contain automation rules.")
3637
pushAllCmd.PersistentFlags().StringVarP(&pushAppGroupsSubDir, "push-app-groups-subdirectory", "", defaultPushAppGroupsSubDir, "Sub-directory within push-directory, to contain App Groups.")
3738
pushAllCmd.PersistentFlags().StringVarP(&pushPolicyMappingsSubDir, "push-policy-mappings-subdirectory", "", defaultPushPolicyMappingsSubDir, "Sub-directory within push-directory, to contain Policy Mappings.")
39+
pushAllCmd.PersistentFlags().StringVarP(&pushKyvernoPoliciesSubDir, "push-kyverno-policies-subdirectory", "", defaultPushKyvernoPoliciesSubDir, "Sub-directory within push-directory, to contain Kyverno policies.")
3840
pushAllCmd.PersistentFlags().BoolVarP(&warningsAreFatal, "warnings-are-fatal", "", false, "Treat warnings as a failure and exit with a non-zero status. For example, if pushing OPA policies and automation rules succeeds, but pushing policies configuration fails because the settings.yaml file is not present.")
3941
pushCmd.AddCommand(pushAllCmd)
4042
}
@@ -51,7 +53,7 @@ var pushAllCmd = &cobra.Command{
5153
}
5254

5355
org := configurationObject.Options.Organization
54-
const resourcesTypeToPush = 5
56+
const resourcesTypeToPush = 6
5557

5658
var numWarnings, numFailures int
5759
logrus.Infoln("Pushing OPA policies, automation rules, and policies configuration to Insights.")
@@ -120,6 +122,25 @@ var pushAllCmd = &cobra.Command{
120122
}
121123
}
122124

125+
absPushKyvernoPoliciesDir := filepath.Join(pushDir, pushKyvernoPoliciesSubDir)
126+
_, err = os.Stat(absPushKyvernoPoliciesDir)
127+
if err != nil {
128+
logrus.Warnf("Unable to push Kyverno policies (%s): %v", absPushKyvernoPoliciesDir, err)
129+
numWarnings++
130+
} else {
131+
policies, err := kyverno.GetPolicyFilesForPush(absPushKyvernoPoliciesDir)
132+
if err != nil {
133+
logrus.Errorf("Unable to read Kyverno policy files: %v", err)
134+
numFailures++
135+
} else {
136+
err = kyverno.PushKyvernoPolicies(client, policies, org, pushDelete, pushDryRun)
137+
if err != nil {
138+
logrus.Errorf("Unable to push Kyverno policies: %v", err)
139+
numFailures++
140+
}
141+
}
142+
}
143+
123144
if numFailures == 0 && numWarnings == 0 {
124145
logrus.Infoln("Push succeeded.")
125146
return

pkg/cli/push_app_groups.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ import (
2121
)
2222

2323
var pushAppGroupsSubDir string
24-
var defaultPushAppGroupsSubDir = "app-groups"
24+
25+
const defaultPushAppGroupsSubDir = "app-groups"
2526

2627
func init() {
2728
// This flag sets a variable defined in the parent `push` command.

0 commit comments

Comments
 (0)