Skip to content

Commit cac49c4

Browse files
authored
feat: add support to validate via Kong API (#502)
Fix #190
1 parent a0569ea commit cac49c4

File tree

5 files changed

+322
-12
lines changed

5 files changed

+322
-12
lines changed

cmd/common.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ func workspaceExists(ctx context.Context, config utils.KongClientConfig, workspa
5353
return exists, nil
5454
}
5555

56+
func getWorkspaceName(workspaceFlag string, targetContent *file.Content) string {
57+
if workspaceFlag != targetContent.Workspace && workspaceFlag != "" {
58+
cprint.DeletePrintf("Warning: Workspace '%v' specified via --workspace flag is "+
59+
"different from workspace '%v' found in state file(s).\n", workspaceFlag, targetContent.Workspace)
60+
return workspaceFlag
61+
}
62+
return targetContent.Workspace
63+
}
64+
5665
func syncMain(ctx context.Context, filenames []string, dry bool, parallelism,
5766
delay int, workspace string) error {
5867

@@ -70,16 +79,9 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism,
7079
return err
7180
}
7281

73-
var wsConfig utils.KongClientConfig
74-
var workspaceName string
7582
// prepare to read the current state from Kong
76-
if workspace != targetContent.Workspace && workspace != "" {
77-
cprint.DeletePrintf("Warning: Workspace '%v' specified via --workspace flag is "+
78-
"different from workspace '%v' found in state file(s).\n", workspace, targetContent.Workspace)
79-
workspaceName = workspace
80-
} else {
81-
workspaceName = targetContent.Workspace
82-
}
83+
var wsConfig utils.KongClientConfig
84+
workspaceName := getWorkspaceName(workspace, targetContent)
8385
wsConfig = rootConfig.ForWorkspace(workspaceName)
8486

8587
// load Kong version after workspace

cmd/validate.go

+128-3
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,30 @@ import (
66
"github.com/kong/deck/dump"
77
"github.com/kong/deck/file"
88
"github.com/kong/deck/state"
9+
"github.com/kong/deck/utils"
10+
"github.com/kong/deck/validate"
911
"github.com/spf13/cobra"
1012
)
1113

1214
var (
1315
validateCmdKongStateFile []string
1416
validateCmdRBACResourcesOnly bool
17+
validateOnline bool
18+
validateWorkspace string
19+
validateParallelism int
1520
)
1621

1722
// validateCmd represents the diff command
1823
var validateCmd = &cobra.Command{
1924
Use: "validate",
2025
Short: "Validate the state file",
2126
Long: `The validate command reads the state file and ensures validity.
22-
2327
It reads all the specified state files and reports YAML/JSON
2428
parsing issues. It also checks for foreign relationships
2529
and alerts if there are broken relationships, or missing links present.
30+
2631
No communication takes places between decK and Kong during the execution of
27-
this command.
32+
this command unless --online flag is used.
2833
`,
2934
Args: validateNoArgs,
3035
RunE: func(cmd *cobra.Command, args []string) error {
@@ -51,11 +56,16 @@ this command.
5156
return err
5257
}
5358
// this catches foreign relation errors
54-
_, err = state.Get(rawState)
59+
ks, err := state.Get(rawState)
5560
if err != nil {
5661
return err
5762
}
5863

64+
if validateOnline {
65+
if errs := validateWithKong(cmd, ks, targetContent); len(errs) != 0 {
66+
return validate.ErrorsWrapper{Errors: errs}
67+
}
68+
}
5969
return nil
6070
},
6171
PreRunE: func(cmd *cobra.Command, args []string) error {
@@ -67,6 +77,107 @@ this command.
6777
},
6878
}
6979

80+
func validateWithKong(cmd *cobra.Command, ks *state.KongState, targetContent *file.Content) []error {
81+
ctx := cmd.Context()
82+
// make sure we are able to connect to Kong
83+
_, err := fetchKongVersion(ctx, rootConfig)
84+
if err != nil {
85+
return []error{fmt.Errorf("couldn't fetch Kong version: %w", err)}
86+
}
87+
88+
workspaceName := validateWorkspace
89+
if validateWorkspace != "" {
90+
// check if workspace exists
91+
workspaceName := getWorkspaceName(validateWorkspace, targetContent)
92+
workspaceExists, err := workspaceExists(ctx, rootConfig, workspaceName)
93+
if err != nil {
94+
return []error{err}
95+
}
96+
if !workspaceExists {
97+
return []error{fmt.Errorf("workspace doesn't exist: %s", workspaceName)}
98+
}
99+
}
100+
101+
wsConfig := rootConfig.ForWorkspace(workspaceName)
102+
kongClient, err := utils.GetKongClient(wsConfig)
103+
if err != nil {
104+
return []error{err}
105+
}
106+
107+
opts := validate.ValidatorOpts{
108+
Ctx: ctx,
109+
State: ks,
110+
Client: kongClient,
111+
Parallelism: validateParallelism,
112+
RBACResourcesOnly: validateCmdRBACResourcesOnly,
113+
}
114+
validator := validate.NewValidator(opts)
115+
return validator.Validate()
116+
}
117+
118+
// ensureGetAllMethod ensures at init time that `GetAll()` method exists on the relevant structs.
119+
// If the method doesn't exist, the code will panic. This increases the likelihood of catching such an
120+
// error during manual testing.
121+
func ensureGetAllMethods() error {
122+
// let's make sure ASAP that all resources have the expected GetAll method
123+
dummyEmptyState, _ := state.NewKongState()
124+
if _, err := utils.CallGetAll(dummyEmptyState.Services); err != nil {
125+
return err
126+
}
127+
if _, err := utils.CallGetAll(dummyEmptyState.ACLGroups); err != nil {
128+
return err
129+
}
130+
if _, err := utils.CallGetAll(dummyEmptyState.BasicAuths); err != nil {
131+
return err
132+
}
133+
if _, err := utils.CallGetAll(dummyEmptyState.CACertificates); err != nil {
134+
return err
135+
}
136+
if _, err := utils.CallGetAll(dummyEmptyState.Certificates); err != nil {
137+
return err
138+
}
139+
if _, err := utils.CallGetAll(dummyEmptyState.Consumers); err != nil {
140+
return err
141+
}
142+
if _, err := utils.CallGetAll(dummyEmptyState.Documents); err != nil {
143+
return err
144+
}
145+
if _, err := utils.CallGetAll(dummyEmptyState.HMACAuths); err != nil {
146+
return err
147+
}
148+
if _, err := utils.CallGetAll(dummyEmptyState.JWTAuths); err != nil {
149+
return err
150+
}
151+
if _, err := utils.CallGetAll(dummyEmptyState.KeyAuths); err != nil {
152+
return err
153+
}
154+
if _, err := utils.CallGetAll(dummyEmptyState.Oauth2Creds); err != nil {
155+
return err
156+
}
157+
if _, err := utils.CallGetAll(dummyEmptyState.Plugins); err != nil {
158+
return err
159+
}
160+
if _, err := utils.CallGetAll(dummyEmptyState.Routes); err != nil {
161+
return err
162+
}
163+
if _, err := utils.CallGetAll(dummyEmptyState.SNIs); err != nil {
164+
return err
165+
}
166+
if _, err := utils.CallGetAll(dummyEmptyState.Targets); err != nil {
167+
return err
168+
}
169+
if _, err := utils.CallGetAll(dummyEmptyState.Upstreams); err != nil {
170+
return err
171+
}
172+
if _, err := utils.CallGetAll(dummyEmptyState.RBACEndpointPermissions); err != nil {
173+
return err
174+
}
175+
if _, err := utils.CallGetAll(dummyEmptyState.RBACRoles); err != nil {
176+
return err
177+
}
178+
return nil
179+
}
180+
70181
func init() {
71182
rootCmd.AddCommand(validateCmd)
72183
validateCmd.Flags().BoolVar(&validateCmdRBACResourcesOnly, "rbac-resources-only",
@@ -75,4 +186,18 @@ func init() {
75186
"state", "s", []string{"kong.yaml"}, "file(s) containing Kong's configuration.\n"+
76187
"This flag can be specified multiple times for multiple files.\n"+
77188
"Use '-' to read from stdin.")
189+
validateCmd.Flags().BoolVar(&validateOnline, "online",
190+
false, "perform validations against Kong API. When this flag is used, validation is done\n"+
191+
"via communication with Kong. This increases the time for validation but catches \n"+
192+
"significant errors. No resource is created in Kong.")
193+
validateCmd.Flags().StringVarP(&validateWorkspace, "workspace", "w",
194+
"", "validate configuration of a specific workspace "+
195+
"(Kong Enterprise only).\n"+
196+
"This takes precedence over _workspace fields in state files.")
197+
validateCmd.Flags().IntVar(&validateParallelism, "parallelism",
198+
10, "Maximum number of concurrent requests to Kong.")
199+
200+
if err := ensureGetAllMethods(); err != nil {
201+
panic(err.Error())
202+
}
78203
}

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func registerSignalHandler() context.Context {
1818
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
1919

2020
go func() {
21+
defer signal.Stop(sigs)
2122
sig := <-sigs
2223
fmt.Println("received", sig, ", terminating...")
2324
cancel()

utils/utils.go

+14
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"net/url"
66
"os"
77
"path/filepath"
8+
"reflect"
89
"regexp"
910
"strings"
1011
)
@@ -52,3 +53,16 @@ func NameToFilename(name string) string {
5253
func FilenameToName(filename string) string {
5354
return strings.ReplaceAll(filename, url.PathEscape(string(os.PathSeparator)), string(os.PathSeparator))
5455
}
56+
57+
func CallGetAll(obj interface{}) (reflect.Value, error) {
58+
// call GetAll method on entity
59+
var result reflect.Value
60+
method := reflect.ValueOf(obj).MethodByName("GetAll")
61+
if !method.IsValid() {
62+
return result, fmt.Errorf("GetAll() method not found for type '%v'. "+
63+
"Please file a bug with Kong Inc", reflect.ValueOf(obj).Type())
64+
}
65+
entities := method.Call([]reflect.Value{})[0].Interface()
66+
result = reflect.ValueOf(entities)
67+
return result, nil
68+
}

0 commit comments

Comments
 (0)